Compare commits

...
383 Commits
Author SHA1 Message Date
Ruben Fiszelandrubenfiszel 068661aa21 chore(main): release 1.96.0 (#1538)
* chore(main): release 1.96.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-08 19:43:28 +02:00
Ruben Fiszel 13242abff1 feat: add support for full fleged apps (react, svelte, vue) (#1536)
* add react support

* use whoami

* update

* update

* update

* create react app

* all

* sqlx
2023-05-08 19:10:45 +02:00
Faton Ramadani 686f5bbe18 feat(frontend): app modal (#1518)
* feat(frontend): app modal wip

* feat(frontend): Modal

* feat(frontend): fix modal title

* feat(frontend): Merge main

* feat(frontend): fix build

* feat(frontend): use ResolveConfig + rename prop

* feat(frontend): use ResolveConfig + rename prop
2023-05-08 18:04:43 +02:00
Faton Ramadani bef829d480 feat(frontend): Add label, description, input style + add displayType… (#1540)
* feat(frontend): Add label, description, input style + add displayType prop

* feat(frontend): add large gap prop + use ResolveConfig
2023-05-08 18:04:18 +02:00
Faton Ramadani 3a291f7108 feat(frontend): Add a custom deepEqualWithOrderedArray to handle orde… (#1537)
* feat(frontend): Add a custom deepEqualWithOrderedArray to handle ordered arrays comparaison

* feat(frontend): Add a custom deepEqualWithOrderedArray to handle ordered arrays comparaison

* feat(frontend): simplify
2023-05-08 14:25:13 +02:00
Ruben Fiszel 0bd0f6b4d1 fix login grid for providers <=2 2023-05-07 15:20:40 +02:00
Ruben Fiszel df9f6acc7a avoid the need for caddy for dev 2023-05-07 11:30:03 +02:00
Ruben Fiszel f43920e34f add initial typescript client 2023-05-07 01:17:42 +02:00
Ruben Fiszel a8f3532919 replaceState on deploy 2023-05-06 15:34:30 +02:00
Ruben Fiszel ba4289b9f1 cli nit 2023-05-06 13:41:56 +02:00
Ruben Fiszel 41fbb4fbf9 nit failure_module 2023-05-06 12:19:42 +02:00
Ruben Fiszelandrubenfiszel dc8f0d11da chore(main): release 1.95.1 (#1535)
* chore(main): release 1.95.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-06 12:09:45 +02:00
Ruben Fiszel c23e6b4534 assign static crate for rustpython-parser 2023-05-06 12:02:52 +02:00
Ruben Fiszel a74deb7445 cli improvements 2023-05-06 11:44:54 +02:00
Ruben Fiszel 3598a650a8 cli nits 2023-05-06 11:11:22 +02:00
Ruben Fiszel e585e3aea2 fix(cli): cli flow sync improvements 2023-05-06 11:06:39 +02:00
Ruben Fiszelandrubenfiszel 36ecb3a279 chore(main): release 1.95.0 (#1532)
* chore(main): release 1.95.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-05 23:26:28 +02:00
Ruben Fiszel 93ae0d71a4 feat(cli): split flows inline scripts 2023-05-05 23:26:03 +02:00
Faton Ramadani 15f1c947bb feat(frontend): App stepper (#1529)
* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): stepper done

* feat(frontend): Fix colors

* feat(frontend): fix build

* feat(frontend): app stepper done

* feat(frontend): app stepper done

* feat(frontend): add editor mode

* feat(frontend): remove unnecessary reactivity

* feat(frontend): python default code

* feat(frontend): remove submit

* feat(frontend): pre-connect

* feat(frontend): Adapt AppStepper

* feat(frontend): add errorHandledByComponent

* feat(frontend): remove button + fix runStep

* feat(frontend): fix overflow
2023-05-05 17:15:40 +02:00
Faton Ramadani 85c0d939f5 feat(frontend): App Schema Form component (#1533)
* feat(frontend): app static form

* feat(frontend): working schemaForm

* feat(frontend): add lightmode

* feat(frontend): sync values direclty

* feat(frontend): remove dev code
2023-05-05 16:40:07 +02:00
Faton RamadaniandRuben Fiszel 1be4658150 feat(frontend): Merge run configuration + triggers (#1530)
* feat(frontend): Merge run configuration + triggers

* feat(frontend): merge main + fix add dependendy

* feat(frontend): merge main + fix add dependendy

* feat(frontend): merge main + fix add dependendy

* Update ScriptRunConfiguration.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-05-05 16:20:01 +02:00
Ruben Fiszel 845343c369 fix html frame clickable in preview mode 2023-05-05 15:17:34 +02:00
Ruben Fiszel aaef5b5ac4 support purejs hashing if subtle not available 2023-05-05 15:10:45 +02:00
Ruben Fiszel 2a958a9d1d fix default value in python 2023-05-05 10:03:28 +02:00
Ruben Fiszel d0aebadedf clarify default value for property editor of flows 2023-05-05 09:57:30 +02:00
Ruben Fiszel 8791a86a93 feat(backend): default parameters are used in python if missing from args 2023-05-05 09:44:53 +02:00
Ruben Fiszel f90b376319 make yaml the default on the cli 2023-05-05 09:08:28 +02:00
Ruben Fiszelandrubenfiszel 2a7812c434 chore(main): release 1.94.0 (#1525)
* chore(main): release 1.94.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-05 02:51:13 +02:00
Ruben Fiszel 73738c0873 fix(cli): refactor cli (#1531)
* all

* cliUpgrade

* refactor entire cli
2023-05-05 02:51:01 +02:00
Ruben Fiszel f3ec9ca09d make the group 'all' undeletable 2023-05-04 19:22:53 +02:00
Ruben Fiszel b4867f12bb fix(frontend): preserve customise arguments 2023-05-04 15:39:15 +02:00
Faton Ramadani 32f04c7968 feat(frontend): add eval badge + alert (#1522)
* feat(frontend): add eval badge + alert

* fix(frontend): adapt eval badge

* fix(frontend): fix  eval badge
2023-05-04 12:02:40 +02:00
Faton Ramadani b423eec019 fix(frontend): fix ArrayStaticInputEditor width (#1528) 2023-05-04 11:55:18 +02:00
Faton Ramadani f248c09655 fix(frontend): fix select width (#1526) 2023-05-04 09:35:35 +02:00
Ruben Fiszel d795d868fe fix cli in yaml mode 2023-05-04 08:36:19 +02:00
Ruben Fiszel af3c89efc4 fix tabs configuration 2023-05-03 23:59:53 +02:00
Ruben Fiszel a07e3e8438 fix(backend): fix python transformers 2023-05-03 23:27:14 +02:00
Ruben Fiszel 1040b8be3b handle better diff on yaml for cli 2023-05-03 19:27:10 +02:00
Ruben Fiszelandrubenfiszel 57d7876d76 chore(main): release 1.93.1 (#1524)
* chore(main): release 1.93.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-03 16:44:03 +02:00
Ruben Fiszel 03e6017860 fix(cli): add yaml support for cli 2023-05-03 16:40:46 +02:00
Ruben Fiszel 6a108d6b7a chore(main): release 1.93.0 (#1521) 2023-05-03 16:24:20 +02:00
Ruben Fiszel fd070c0e75 add cli yaml support (#1520)
* add cli yaml support

* use the new yaml option
2023-05-03 16:21:24 +02:00
Henri Courdent 7889de033a Replaced background scripts by background runnables -> validate PR 165 on wmildocs before (#1523) 2023-05-03 16:10:55 +02:00
Ruben Fiszel 0443bb6c37 update README_DEV for mac 2023-05-03 15:17:42 +02:00
Ruben Fiszel 727d946d52 fix tests 2023-05-03 14:31:54 +02:00
Ruben Fiszelandrubenfiszel 5a94f5bef2 chore(main): release 1.93.0 (#1519)
* chore(main): release 1.93.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-03 14:16:49 +02:00
Ruben Fiszel dd18e4d0d5 fix reactivity issue wrt to updating worldStore on state output already existing 2023-05-03 14:16:07 +02:00
Ruben Fiszel f2457faf8a nit select undefined item 2023-05-03 13:30:59 +02:00
Ruben Fiszel 7ac96771a5 fix(frontend): deploy path for apps 2023-05-03 13:24:25 +02:00
Ruben Fiszel 1f11340143 minor menu nits 2023-05-03 12:02:12 +02:00
Ruben Fiszel bff52e576d fix hover issue for popups 2023-05-03 11:49:03 +02:00
Ruben Fiszel 16d8dbb29b change wording around background script -> background runnable 2023-05-03 10:51:43 +02:00
Ruben Fiszel 392d0f8b87 feat(frontend): add recompute others to background scripts 2023-05-03 10:48:13 +02:00
Ruben Fiszel 10602a370e more consistent clear behavior for frontend apps 2023-05-03 09:54:13 +02:00
Ruben Fiszelandrubenfiszel 8a430a037b chore(main): release 1.92.2 (#1517)
* chore(main): release 1.92.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-03 01:03:58 +02:00
Ruben Fiszel 82c4d6629e fix(go-client): fix go-client gen 2023-05-03 01:00:35 +02:00
Ruben Fiszelandrubenfiszel 4437f9488b chore(main): release 1.92.1 (#1516)
* chore(main): release 1.92.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-03 00:52:23 +02:00
Ruben Fiszel df333d9739 fix(go-client): fix go-client gen 2023-05-03 00:49:03 +02:00
Ruben Fiszel 17e6cd73a3 fix tests 2023-05-03 00:38:43 +02:00
Ruben Fiszelandrubenfiszel 512cebf8eb chore(main): release 1.92.0 (#1512)
* chore(main): release 1.92.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-03 00:27:36 +02:00
Faton Ramadani f6ff8ca232 fix(frontend): fix small ui issues (#1513) 2023-05-03 00:23:38 +02:00
Ruben Fiszel cedc3d6f2c display type for background scripts as well 2023-05-03 00:17:44 +02:00
Ruben Fiszel e9f2ffd61e little migration fix 2023-05-03 00:12:28 +02:00
Ruben Fiszel 607c803be9 feat(frontend): background script can run script and flows (#1515)
* all

* progress

* all

* all
2023-05-03 00:10:41 +02:00
Ruben Fiszel a70d461659 hide deleted background scripts 2023-05-02 14:52:11 +02:00
Ruben Fiszel b52745b3a5 fix show_archived 2023-05-02 14:12:06 +02:00
Ruben Fiszel 85477ba310 improve bar style 2023-05-02 12:33:06 +02:00
Ruben Fiszel 64065c17f3 feat(frontend): add labels as table action 2023-05-02 12:18:54 +02:00
Ruben Fiszel 2ab1714dfa feat(frontend): add labels as table action 2023-05-02 12:17:32 +02:00
Henri CourdentandFaton Ramadani dc34230235 Tooltipin (#1502)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-05-02 11:20:02 +02:00
Ruben Fiszel 62acbb5ab3 feat(frontend): allow running eval in every field 2023-05-02 09:59:18 +02:00
dependabot[bot] f9fbf39201 chore(deps-dev): bump @typescript-eslint/eslint-plugin in /frontend (#1511)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.59.1 to 5.59.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.59.2/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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-05-01 19:09:59 +00:00
dependabot[bot] 33c248fb2c chore(deps-dev): bump cssnano from 6.0.0 to 6.0.1 in /frontend (#1510)
Bumps [cssnano](https://github.com/cssnano/cssnano) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/cssnano/cssnano/releases)
- [Commits](https://github.com/cssnano/cssnano/compare/cssnano@6.0.0...cssnano@6.0.1)

---
updated-dependencies:
- dependency-name: cssnano
  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-05-01 19:07:39 +00:00
dependabot[bot] bf1fecc903 chore(deps-dev): bump prettier from 2.8.7 to 2.8.8 in /frontend (#1509)
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.7 to 2.8.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.7...2.8.8)

---
updated-dependencies:
- dependency-name: prettier
  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-05-01 19:06:42 +00:00
dependabot[bot] 24c4143f82 chore(deps-dev): bump vite from 4.3.1 to 4.3.3 in /frontend (#1508)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.1 to 4.3.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.3/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 19:06:25 +00:00
dependabot[bot] 25c5add90e chore(deps): bump lucide-svelte from 0.176.0 to 0.192.0 in /frontend (#1507)
Bumps [lucide-svelte](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-svelte) from 0.176.0 to 0.192.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/v0.192.0/packages/lucide-svelte)

---
updated-dependencies:
- dependency-name: lucide-svelte
  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-05-01 19:05:53 +00:00
dependabot[bot] 40f52ea5a7 chore(deps-dev): bump @sveltejs/kit from 1.15.6 to 1.15.9 in /frontend (#1506)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.15.6 to 1.15.9.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.15.9/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  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-05-01 19:03:00 +00:00
dependabot[bot] c9098456eb chore(deps-dev): bump @types/node from 18.16.0 to 18.16.3 in /frontend (#1505)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.16.0 to 18.16.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  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-05-01 19:02:31 +00:00
dependabot[bot] e52a2faf6e chore(deps-dev): bump tailwindcss from 3.3.1 to 3.3.2 in /frontend (#1504)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.1...v3.3.2)

---
updated-dependencies:
- dependency-name: tailwindcss
  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-05-01 19:02:26 +00:00
dependabot[bot] ff82d6c42b chore(deps): bump highlight.js from 11.7.0 to 11.8.0 in /frontend (#1503)
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 11.7.0 to 11.8.0.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/11.7.0...11.8.0)

---
updated-dependencies:
- dependency-name: highlight.js
  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-05-01 19:01:35 +00:00
Ruben Fiszel 940589d127 styling nits 2023-05-01 19:32:50 +02:00
Ruben Fiszel 5c67011a11 styling nits 2023-05-01 19:29:45 +02:00
Ruben Fiszelandrubenfiszel e485943d48 chore(main): release 1.91.0 (#1495)
* chore(main): release 1.91.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-05-01 18:58:24 +02:00
Ruben Fiszel f7374c8204 feat: add drafts for apps 2023-05-01 18:13:57 +02:00
Ruben Fiszel 9988a51173 add fork menu back for flows 2023-05-01 09:47:13 +02:00
Ruben Fiszel 41817b855b nit dirtyStore 2023-05-01 09:44:30 +02:00
Ruben Fiszel a1966427e8 feat: introduce draft for flows 2023-05-01 09:31:22 +02:00
Ruben Fiszel bda06ac0e5 Update ScriptBuilder.svelte (#1496) 2023-05-01 02:54:42 +02:00
Ruben Fiszel 421048c222 nit 2023-04-30 17:41:31 +02:00
Ruben Fiszel cddd2e0c1b update readme 2023-04-30 17:31:47 +02:00
Ruben Fiszel 5d64185152 fix flow update update schedules 2023-04-30 16:18:43 +02:00
Ruben Fiszel 88e37fe0be feat: introduce backend persisted draft systems for scripts 2023-04-30 15:56:52 +02:00
Ruben Fiszel d5f07cc788 reword Use as template/Duplicate Fork 2023-04-29 10:22:01 +02:00
Ruben Fiszelandrubenfiszel 4ea7e1aa12 chore(main): release 1.90.0 (#1455)
* chore(main): release 1.90.0

* Apply automatic changes

* Apply automatic changes

* update api client

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-29 09:45:03 +02:00
Ruben Fiszel 5ca8bbcfdd remove windmillhub submodule 2023-04-28 23:15:27 +02:00
Ruben Fiszel 2815f1ec71 feat(frontend): add an eval input component for flow (#1494)
* fix sqlx

* all
2023-04-28 23:09:28 +02:00
Henri CourdentandRuben Fiszel 7277056ef0 First wave of tooltips linking to docs (#1493)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-28 19:52:30 +02:00
Faton Ramadani 52e9887752 Fix frontend scripts settings (#1492)
* fix(frontend): fix recomputa all

* fix(frontend): fix frontend scripts

* fix(frontend): fix frontend scripts

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): fix isScriptByNameDefined

* fix(frontend): fix isScriptByNameDefined

* fix(frontend): fix

* fix(frontend): remove console.log

* fix(frontend): fix multiselect

* fix(frontend): revert changes

* fix(frontend): add migration code

* fix(frontend): fix isRunnableDefined
2023-04-28 19:17:32 +02:00
Faton Ramadani fb05a09955 fix(frontend): fix recomputa all (#1491) 2023-04-28 16:27:25 +02:00
Faton Ramadani 41a24ecd36 feat(frontend): adapt style (#1488) 2023-04-28 15:35:01 +02:00
Ruben Fiszel 1ba9999c18 fix sqlx 2023-04-28 12:59:21 +02:00
Faton Ramadani 05c837e64f feat(frontend): refactor inline script (#1480)
* feat(frontend): refactor inline script wip

* feat(frontend): refactor inline script wip

* feat(frontend): clean up

* feat(frontend): fix run configuration for triggerable

* feat(frontend): fix autoRefresh issues

* feat(frontend): fix autoRefresh issues

* feat(frontend): modify actions + remove reactivity

* feat(frontend): wip

* feat(frontend): fix RecomputeAllComponnet

* feat(frontend): add intermediate state when the language has not yet been selected

* feat(frontend): fix connection

* feat(frontend): fix wording

* feat(frontend): fix bg trigers list

* feat(frontend): restore

* feat(frontend): restore

* feat(frontend): add missing migration
2023-04-28 12:56:06 +02:00
Faton Ramadani cf59cc04ef fix(frontend): fix apply connection (#1487) 2023-04-28 11:46:04 +02:00
Ruben Fiszel 5a98ae1606 treat null accordingly in arginfo 2023-04-28 00:52:08 +02:00
Ruben Fiszel de9abd129d feat(backend): add EXIT_AFTER_NO_JOB_FOR_SECS for ephemeral workers 2023-04-28 00:09:48 +02:00
Ruben Fiszel 324d4f5e9e feat(backend): expose tag in the job (#1486)
* fix

* update tag

* update tag

* update tag

* fix
2023-04-27 19:09:53 +02:00
Ryan Rich 6f8616fb27 fix(frontend): reset ui job loading state when submitting preview job triggers error (#1483)
* fix(frontend): catch errors when submitting preview job

* fix(frontend): catch errors when submitting preview job (simplified)

* fix(frontend): also reset currentId
2023-04-27 18:39:37 +02:00
Ruben Fiszel f568bb1bd8 fix script in schedule should use latest hash 2023-04-27 18:03:12 +02:00
Ruben Fiszel 1d6d60bace add documentationLink to pageHeader 2023-04-27 17:27:12 +02:00
Ruben Fiszel 38addd348c fix cloudflare preview 2023-04-27 12:19:41 +02:00
Faton Ramadani d915f6b004 fix(frontend): register applyConnection as a callback to remove unnecessary reactivit (#1485) 2023-04-27 11:37:40 +02:00
Faton Ramadani 430c73399b fix(frontend): close the modal before deleting a form modal (#1484)
* fix(frontend): close the modal before deleting a form modal

* fix(frontend): use componentControl
2023-04-27 10:27:00 +02:00
Ruben Fiszel c164c0ff29 fix stale comment 2023-04-27 07:42:40 +02:00
Ruben Fiszel 4387db275f fix run_wait_result polling + make it configurable 2023-04-27 07:35:53 +02:00
Ruben Fiszel 95e52b2d74 fix go json parsing 2023-04-26 21:45:26 +02:00
Ruben Fiszel 0b7bad3816 feat(backend): add JOB_RETENTION_SECS to delete completed jobs completed after expiration period 2023-04-26 19:34:54 +02:00
Ruben Fiszel 79f46d08ac improve run_wait_result latency 2023-04-26 17:56:39 +02:00
Faton Ramadani 7ad496ad3f feat(frontend): Small style fix (#1473)
* feat(frontend): Small style fix

* feat(frontend): fix button dropdown z-index

* feat(frontend): fix button dropdown z-index

* feat(frontend): revert

* feat(frontend): revert
2023-04-26 14:27:38 +02:00
Ádám Kovács 7adacd4c9f feat(frontend): Add new integration icons (#1479) 2023-04-26 14:27:11 +02:00
Ruben Fiszel 25bf0b3675 update g/all to f/examples 2023-04-26 09:14:02 +02:00
Ruben Fiszel 4a34e83b56 minor worker tags nit 2023-04-26 01:54:09 +02:00
Ádám Kovács 50d814c3dc fix(frontend): Flow editor design updates (#1477)
* fix(frontend): Add seconds input to flow retries

* fix(frontend): Update flow branch editor styling
2023-04-25 21:20:44 +02:00
Faton Ramadani b60a7f63d0 fix(frontend): Fix frontend script (#1476)
* fix(frontend): fix frontend script intendation

* feat(frontend): wip

* feat(frontend): fix reload all

* feat(frontend): add missing donePromise call

* feat(frontend): move donePromise in setResult

* feat(frontend): revert refresh
2023-04-25 15:46:45 +02:00
Faton Ramadani 47f0f35236 feat(frontend): Schedules run now (#1475)
* feat(frontend): run now

* feat(frontend): run now

* feat(frontend): add missing await + add toast action to view run page

* feat(frontend): fix path

* feat(frontend): fix path
2023-04-25 14:47:13 +02:00
Faton Ramadani d8529ff3ed fix(frontend): Allow 0 as select default value (#1474) 2023-04-25 12:28:57 +02:00
Ádám Kovács 8fd10b1f58 fix(frontend): Minor app editor updates (#1458)
* fix(frontend): Update clearable inputs

* update icon picker popup

* update
2023-04-25 12:28:27 +02:00
Faton Ramadani 8b763249cb feat(frontend): Support TS union type with a select field (#1457)
* feat(frontend): Support TS union type with a select field

* feat(frontend): Support TS union type with a select field
2023-04-24 22:57:50 +02:00
dependabot[bot] 35a9de5886 chore(deps-dev): bump @typescript-eslint/parser in /frontend (#1469)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.59.0 to 5.59.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.59.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-24 19:12:51 +00:00
dependabot[bot] 679ccbb781 chore(deps-dev): bump @types/node from 18.15.11 to 18.16.0 in /frontend (#1471)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.15.11 to 18.16.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  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-24 19:12:30 +00:00
dependabot[bot] 91e482ee60 chore(deps): bump node from 19-alpine to 20-alpine (#1459)
Bumps node from 19-alpine to 20-alpine.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 21:12:37 +02:00
dependabot[bot] a2150b7ccf chore(deps-dev): bump @types/lodash in /frontend (#1470)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.192 to 4.14.194.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  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-24 19:08:40 +00:00
dependabot[bot] bb72145a41 chore(deps-dev): bump @typescript-eslint/eslint-plugin in /frontend (#1468)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.57.0 to 5.59.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.59.1/packages/eslint-plugin)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 19:06:33 +00:00
dependabot[bot] 38abe0f98e chore(deps): bump tailwind-merge from 1.11.0 to 1.12.0 in /frontend (#1467)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  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-24 19:05:55 +00:00
dependabot[bot] 3ee001761a chore(deps-dev): bump postcss from 8.4.21 to 8.4.23 in /frontend (#1466)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.23)

---
updated-dependencies:
- dependency-name: postcss
  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-24 19:05:13 +00:00
dependabot[bot] 6b342a513f chore(deps-dev): bump @zerodevx/svelte-toast in /frontend (#1464)
Bumps [@zerodevx/svelte-toast](https://github.com/zerodevx/svelte-toast) from 0.8.2 to 0.9.3.
- [Release notes](https://github.com/zerodevx/svelte-toast/releases)
- [Commits](https://github.com/zerodevx/svelte-toast/compare/v0.8.2...v0.9.3)

---
updated-dependencies:
- dependency-name: "@zerodevx/svelte-toast"
  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-24 19:03:11 +00:00
dependabot[bot] d49e4355b8 chore(deps-dev): bump @playwright/test in /frontend (#1463)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.32.1 to 1.32.3.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.32.1...v1.32.3)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  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-24 19:02:44 +00:00
dependabot[bot] 8606655849 chore(deps): bump lucide-svelte from 0.115.0 to 0.176.0 in /frontend (#1461)
Bumps [lucide-svelte](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-svelte) from 0.115.0 to 0.176.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/v0.176.0/packages/lucide-svelte)

---
updated-dependencies:
- dependency-name: lucide-svelte
  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-24 19:02:25 +00:00
dependabot[bot] 867c9624f1 chore(deps-dev): bump vite from 4.2.1 to 4.3.1 in /frontend (#1460)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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-24 19:01:53 +00:00
Ádám Kovács 4a2af1359e fix(frontend): Update direct exports (#1456) 2023-04-24 13:34:41 +02:00
Ruben Fiszel ed9d451246 svelte-package 2 exports 2023-04-24 13:04:07 +02:00
Ádám Kovács c77393c154 fix(frontend): Update CLI login request styling (#1454) 2023-04-24 12:25:53 +02:00
Ruben Fiszel 8259451295 default worker handle other tags 2023-04-24 12:17:38 +02:00
Ruben Fiszel 7ac83acfe2 update readme 2023-04-24 01:35:17 +02:00
Ruben Fiszel 78b4fc9471 fix (#1453) 2023-04-24 00:16:56 +02:00
Ruben Fiszelandrubenfiszel efacaeebf0 chore(main): release 1.89.0 (#1429)
* chore(main): release 1.89.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-23 17:05:46 +02:00
Ruben Fiszel ed049da984 update readme 2023-04-23 17:05:33 +02:00
Ruben Fiszel 722783f7f6 feat(backend): worker groups (#1452)
* tagged scripts

* tagged scripts

* fix

* fix

* fix

* all

* update sqlx
2023-04-23 16:57:23 +02:00
Ruben Fiszel a5ffe22da7 update default tab for hub 2023-04-23 12:18:39 +02:00
Ruben Fiszel 3cf0289921 update self-host instructions 2023-04-23 11:13:57 +02:00
Ádám Kovács 32903d2839 fix(frontend): App color picker overflow issue (#1449) 2023-04-22 09:56:16 +02:00
ELLIOTTCABLE 4b9ec9c080 lsp: Expose 3001 in the Docker image (#1450) 2023-04-22 09:55:34 +02:00
Ruben Fiszel 3f21e9004c move tests to not use njsail 2023-04-22 08:24:54 +02:00
Ruben Fiszel d76e90757e fix(backend): avoid potential conflict between pull from tar and background sync 2023-04-21 23:49:14 +02:00
Ruben Fiszel 5f8a730fdf fix(backend): global cache synco only start if all piptars have been downloaded 2023-04-21 22:21:04 +02:00
Faton RamadaniandRuben Fiszel c6a7a62d46 App templates (#1448)
* feat(backend): workers are instantly ready and sync with global cache in background

* feat(frontend): add templates

* feat(frontend): fix

* feat(frontend): add psql template

* feat(frontend): Move braces

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-21 15:09:18 +02:00
Ádám Kovács 30bf7ad3e9 feat(frontend): Add seconds input (#1445)
* feat(frontend): Add seconds input

* add more instances

* fix header toggles

* add retry calculations
2023-04-21 14:35:50 +02:00
Ruben Fiszel 3d031c7017 feat(backend): only run fully deployed scripts 2023-04-21 12:49:47 +02:00
Ruben Fiszel c0ab27a9e7 fix frontend compile 2023-04-21 10:46:18 +02:00
Faton Ramadani f7a2ed15d2 App UI rework (#1442)
* feat(frontend): app editor ui rework

* feat(frontend): update insert variable

* feat(frontend): fix preview button

* feat(frontend): update toggle group v2

* feat(frontend): fix typo

* feat(frontend): fix build

* feat(frontend): adapt homepage

* feat(frontend): small fix

* feat(frontend): add back type
2023-04-21 10:40:03 +02:00
Ruben Fiszel 670ba51d9b feat(backend): workers are instantly ready and sync with global cache in background 2023-04-21 09:45:25 +02:00
Ruben Fiszel 68ab75169a improve log line for cache syncing 2023-04-21 01:54:58 +02:00
Ruben Fiszel 455e2e9c25 fix untar_all_piptars 2023-04-21 01:02:51 +02:00
Ruben Fiszel ddc04f93c3 better rclone filters 2023-04-21 00:31:39 +02:00
Ruben Fiszel fac32f3ebf better rclone filters 2023-04-20 23:45:31 +02:00
Ruben Fiszel ed87020a83 set relative folder for archive creation 2023-04-20 23:33:53 +02:00
Ruben Fiszel 1f304a7c84 remove deno/gen/windmill from tmp cache 2023-04-20 21:04:17 +02:00
Ruben Fiszel f67062f150 fix owner permission for admins 2023-04-20 20:52:00 +02:00
Ruben Fiszel 96f79bc6cc --s3-no-cache-buckets 2023-04-20 20:50:02 +02:00
Ruben Fiszel 92e5964464 fix tests 2023-04-20 20:17:03 +02:00
Ruben Fiszel 369dd0dac6 feat(backend): global cache refactor for pip using tar for each dependency (#1443)
* cache refactor

* exclude tar from being synced to bucket

* run

* update

* update
2023-04-20 20:05:12 +02:00
Ryan Rich 3c98452f50 feat(frontend): support showing metadata on script add via query param (#1438)
* feat(frontend): support showing metadata on script add via query param

* feat(frontend): snake case query param per pr feedback
2023-04-20 16:35:58 +02:00
Ádám Kovács 46a29b5d27 fix(frontend): Update flow viewer styling (#1441)
* fix(frontend): Update flow viewer styling

* update
2023-04-20 16:35:45 +02:00
Ádám Kovács fe75aa18f2 fix(frontend): Minor update of app default codes (#1440) 2023-04-20 11:38:30 +02:00
Ruben Fiszel 7f886a673b fix python install on save with nsjail 2023-04-20 10:33:11 +02:00
Mohsin Malik 8636801a7a Add UI toggles (#1437)
* Add UI toggles

* Reset .env

* Remove console.log

* Remove more console.logs
2023-04-19 22:48:50 +02:00
Ádám Kovács c8acfbc1ff fix(frontend): Update app default codes (#1432)
* fix(frontend): Update app default codes

* remove console log
2023-04-19 22:46:00 +02:00
Ruben Fiszel 62d196ecec add barrier when num workers > 1 2023-04-19 17:00:03 +02:00
Ádám Kovács 17646130bc fix(frontend): Update app mobile preview width (#1431)
* fix(frontend): App mobile preview width

* fix(frontend): Add min width to app desktop preview
2023-04-19 14:54:51 +02:00
Ádám Kovács 17cb8fc3fa fix(frontend): Update flow tooltip z-indexes (#1433) 2023-04-19 14:54:39 +02:00
Ruben Fiszel 29848ae466 add barrier when num workers > 1 2023-04-19 12:46:25 +02:00
Faton Ramadani 36acbf793b feat(frontend): Add documentation links (#1399)
* feat(frontend): Add documentation link

* feat(frontend): update documentation link style

* feat(frontend): update documentation link
2023-04-19 09:37:57 +02:00
Ruben Fiszel c919827cf8 feat(ee): sync cache in background 2023-04-19 01:15:41 +02:00
Ruben Fiszel 0e77e37fbd feat(ee): sync cache in background 2023-04-19 00:55:51 +02:00
Faton Ramadani 3615fb26fb feat(frontend): reorder array items in app editor (#1426)
* feat(frontend): reorder array items in app editor

* feat(frontend): support tab

* feat(frontend): swap subgrids accordingly

* feat(frontend): fix syncing issues

* feat(frontend): fix syncing issues

* feat(frontend): Fix reactive statement
2023-04-19 00:01:03 +02:00
Ruben Fiszel fa2e38a2f2 only cache on first worker 2023-04-18 21:23:29 +02:00
Faton Ramadani d17323286a feat(frontend): add toast actions (#1411)
* feat(frontend): add toast actions

* feat(frontend): Fix callback

* feat(frontend): Add start from blank

* feat(frontend): support flow as well
2023-04-18 18:32:04 +02:00
Faton Ramadani d96d4a524e fix(frontend): fix copy content button width (#1428) 2023-04-18 18:31:40 +02:00
Ruben Fiszel b8b4308aef minor UX improvements 2023-04-18 10:50:49 +02:00
Ruben Fiszel d036fee6c8 minor UX improvements 2023-04-18 10:40:40 +02:00
Ruben Fiszel 6954d0882e log how much time took each command child 2023-04-18 08:55:26 +02:00
Ruben Fiszelandrubenfiszel e87605d1f5 chore(main): release 1.88.1 (#1425)
* chore(main): release 1.88.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-18 08:30:08 +02:00
Ruben Fiszel 7e3a297c49 minor UX improvements 2023-04-18 08:29:21 +02:00
Ruben Fiszel 1144329972 fix(frontend): fix hub list 2023-04-18 08:25:26 +02:00
Ruben Fiszel cd8f0b5507 revert fix hub list 2023-04-18 08:14:38 +02:00
Ruben Fiszel 26a86ba0d8 improve bash output buffering 2023-04-18 01:42:28 +02:00
Ruben Fiszelandrubenfiszel dbd0dc1aa9 chore(main): release 1.88.0 (#1400)
* chore(main): release 1.88.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-18 00:54:22 +02:00
Ruben Fiszel ba978f254e fix hub list 2023-04-18 00:48:08 +02:00
Ruben Fiszel 22cf2d3552 fix togglegroupbutton 2023-04-18 00:37:26 +02:00
Faton Ramadani 48bbbd0e87 feat(frontend): Unify main lists (#1406)
* feat(frontend): add a dev docker-compose file + update readme

* feat(frontend): Merge Hub items into one tab

* feat(frontend): fix search

* feat(frontend):  fix app filter

* feat(frontend):  fix everything

* feat(frontend):  fix everything

* feat(frontend): revert

* feat(frontend): revert

* feat(frontend): Mibrate old components

* feat(frontend): fix build
2023-04-18 00:23:04 +02:00
Ruben Fiszel 21fb185301 Revert "fix(frontend): Keep selected tab during renaming (#1409)" (#1424)
This reverts commit 82cd048ef4.
2023-04-18 00:18:49 +02:00
Ádám Kovács 82cd048ef4 fix(frontend): Keep selected tab during renaming (#1409) 2023-04-18 00:16:27 +02:00
dependabot[bot] 3f429a4a07 chore(deps-dev): bump stylelint-config-recommended in /frontend (#1417)
Bumps [stylelint-config-recommended](https://github.com/stylelint/stylelint-config-recommended) from 9.0.0 to 12.0.0.
- [Release notes](https://github.com/stylelint/stylelint-config-recommended/releases)
- [Changelog](https://github.com/stylelint/stylelint-config-recommended/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint-config-recommended/compare/9.0.0...12.0.0)

---
updated-dependencies:
- dependency-name: stylelint-config-recommended
  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-18 00:16:09 +02:00
dependabot[bot] 04fa114720 chore(deps-dev): bump typescript from 4.9.5 to 5.0.4 in /frontend (#1419)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.0.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.0.4)

---
updated-dependencies:
- dependency-name: typescript
  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-18 00:15:53 +02:00
Ruben Fiszel 88496a06c0 update package 2023-04-17 23:38:34 +02:00
dependabot[bot] 6c91deb40c chore(deps-dev): bump @sveltejs/package from 1.0.2 to 2.0.2 in /frontend (#1418)
Bumps [@sveltejs/package](https://github.com/sveltejs/kit/tree/HEAD/packages/package) from 1.0.2 to 2.0.2.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/package/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/package@2.0.2/packages/package)

---
updated-dependencies:
- dependency-name: "@sveltejs/package"
  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-17 23:26:33 +02:00
dependabot[bot] c924f560d5 chore(deps-dev): bump @sveltejs/kit from 1.15.2 to 1.15.6 in /frontend (#1422)
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.15.2 to 1.15.6.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.15.6/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  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-17 19:13:33 +00:00
dependabot[bot] deb2c48522 chore(deps-dev): bump monaco-editor-workers in /frontend (#1423)
Bumps [monaco-editor-workers](https://github.com/TypeFox/monaco-components/tree/HEAD/packages/monaco-editor-workers) from 0.36.0 to 0.37.0.
- [Release notes](https://github.com/TypeFox/monaco-components/releases)
- [Changelog](https://github.com/TypeFox/monaco-components/blob/main/packages/monaco-editor-workers/CHANGELOG.md)
- [Commits](https://github.com/TypeFox/monaco-components/commits/HEAD/packages/monaco-editor-workers)

---
updated-dependencies:
- dependency-name: monaco-editor-workers
  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-17 19:09:14 +00:00
dependabot[bot] 608b71db39 chore(deps-dev): bump @sveltejs/adapter-static in /frontend (#1421)
Bumps [@sveltejs/adapter-static](https://github.com/sveltejs/kit/tree/HEAD/packages/adapter-static) from 2.0.1 to 2.0.2.
- [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.2/packages/adapter-static)

---
updated-dependencies:
- dependency-name: "@sveltejs/adapter-static"
  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-17 19:05:37 +00:00
dependabot[bot] 51f27ca1e8 chore(deps-dev): bump @typescript-eslint/parser in /frontend (#1420)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.57.0 to 5.59.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.59.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-17 19:05:23 +00:00
dependabot[bot] 53dac61fb1 chore(deps): bump @tanstack/svelte-table in /frontend (#1416)
Bumps [@tanstack/svelte-table](https://github.com/tanstack/table) from 8.8.4 to 8.8.5.
- [Release notes](https://github.com/tanstack/table/releases)
- [Commits](https://github.com/tanstack/table/compare/v8.8.4...v8.8.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-17 19:01:12 +00:00
dependabot[bot] 12159b7f3f chore(deps-dev): bump svelte-check from 3.1.4 to 3.2.0 in /frontend (#1415)
Bumps [svelte-check](https://github.com/sveltejs/language-tools) from 3.1.4 to 3.2.0.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check-3.1.4...svelte-check-3.2.0)

---
updated-dependencies:
- dependency-name: svelte-check
  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-17 19:00:44 +00:00
Ruben Fiszel 2077c6e5eb fix fullscreen editor 2023-04-17 20:27:38 +02:00
Ruben Fiszel b75b28c3bf add more script builer exit options 2023-04-17 09:13:57 +02:00
Ruben Fiszel 6b49f79cc6 add more script builer exit options 2023-04-16 11:09:01 +02:00
Ruben Fiszel b0bea9416a fix refreshFlow loop 2023-04-16 00:44:34 +02:00
Ruben Fiszel b9175ba14d fix refreshFlow loop 2023-04-16 00:40:54 +02:00
Ruben Fiszel ffeb8e0874 sqlx prepare 2023-04-15 23:48:48 +02:00
Ruben Fiszel 5aa01c567f minor UX improvements 2023-04-15 23:32:13 +02:00
Ruben Fiszel 29fc777c76 minor UX improvements 2023-04-15 23:21:18 +02:00
Ruben Fiszel 4018aa7828 minor UX improvements 2023-04-15 23:10:36 +02:00
Ruben Fiszel 9e1e9a8d3a rework ownership permissions 2023-04-15 23:03:16 +02:00
Ádám Kovács b2a40a0580 fix(frontend): Queued-running jobs preview (#1414)
* fix(frontend): Run preview queued jobs

* styling

* wording
2023-04-15 13:36:54 +02:00
Ádám Kovács accdc1ac59 fix(frontend): Handle scheduled runs in preview (#1413) 2023-04-15 12:03:12 +02:00
Ádám KovácsandFaton Ramadani 4ab023f950 feat(frontend): Add runs preview popup (#1405)
* feat(frontend): Add runs preview popup

* fix(frontend): Remove events from closed drawer

* remove comment

* limit open previews to one

* handle running jobs

* update styling

* position correctly

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-04-15 08:02:04 +00:00
Faton RamadaniandRuben Fiszel 98060ce55d fix(frontend): fix enum sync (#1410)
* fix(frontend): fix enum sync

* Update SchemaEditor.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-15 11:09:40 +02:00
Ruben Fiszel 33437bb847 performance: process completed job in background (#1396)
* compile

* improve
2023-04-15 11:06:51 +02:00
Ruben Fiszel a85ad82b3a add merge group to npm check 2023-04-15 11:03:51 +02:00
Ruben Fiszel d049a13f3c fix grant for input table 2023-04-14 18:34:41 +02:00
Ruben Fiszel e828d2673e feat(frontend): cancellable inline script editor run 2023-04-14 01:08:37 +02:00
Ruben Fiszel 5058d158fc fix python tabSize issue 2023-04-13 23:12:07 +02:00
Ruben Fiszel cb7e686dd9 feat(backend): install python scripts on save 2023-04-13 22:57:53 +02:00
Ruben Fiszel 11b9c8fadb do a test run on first iteration 2023-04-13 21:54:42 +02:00
Ádám Kovács ac489ac2da fix(frontend): Fix app file uploads (#1408)
* fix(frontend): Image upload

* fix

* handle map input error
2023-04-13 21:00:12 +02:00
Ruben Fiszel 4942691a51 consistent workspace query arg 2023-04-13 19:37:58 +02:00
Ruben Fiszel 5c36ca8763 improve saved inputs 2023-04-13 18:32:08 +02:00
Ruben Fiszel 34f48aa1e3 split worker.rs into more files 2023-04-13 17:44:47 +02:00
Faton Ramadani 75a0482ef0 fix(frontend): fix buttons width (#1407)
* fix(frontend): fix buttons width

* fix(frontend): fix order
2023-04-13 16:42:09 +02:00
Faton Ramadani 568f59eefb feat(frontend): Remove gap when button label is empty (#1402) 2023-04-13 09:28:18 +02:00
Ruben Fiszel 8928cfa3d7 fix refreshOn 2023-04-13 09:22:52 +02:00
Ruben Fiszel a5499c26f3 fix(backend): deno uses --no-check 2023-04-13 00:22:46 +02:00
Faton Ramadani 7dc7ecef55 feat(frontend): Update airtable instructions (#1403) 2023-04-12 23:54:23 +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
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 Fiszelandrubenfiszel 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 JellinghausandRuben Fiszel 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 RamadaniandRuben Fiszel 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 Fiszelandrubenfiszel 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 RamadaniandRuben Fiszel 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 VealandRuben Fiszel 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 Fiszelandrubenfiszel 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ácsandRuben Fiszel 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 Fiszelandrubenfiszel 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
508 changed files with 267620 additions and 18730 deletions
+6 -1
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 addres caddy will listen on
CADDY_REVERSE_PROXY=":80"
+1 -1
View File
@@ -40,4 +40,4 @@ jobs:
backend -> target
- name: cargo test
timeout-minutes: 10
run: mkdir frontend/build && cd backend && touch windmill-api/openapi-deref.yaml && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill DISABLE_NSJAIL=false cargo test --all -- --nocapture
run: mkdir frontend/build && cd backend && touch windmill-api/openapi-deref.yaml && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill cargo test --all -- --nocapture
-32
View File
@@ -1,32 +0,0 @@
name: Deploy frontend to s3
on:
push:
branches: [main]
tags: ["*"]
concurrency:
group: ${{ github.ref }}-npmbuild
cancel-in-progress: true
jobs:
npm_build:
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
- uses: actions/setup-node@v3
with:
node-version: 18
- name: "npm build"
timeout-minutes: 15
run:
cd frontend && npm ci && npm run generate-backend-client &&
NODE_OPTIONS="--max-old-space-size=8192" npm run build
- uses: reggionick/s3-deploy@v3
with:
folder: frontend/build
bucket: windmill-frontend
bucket-region: us-east-1
+36 -9
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:
@@ -142,7 +141,28 @@ jobs:
# run: docker kill ${{ steps.docker-container.outputs.id }}
# if: always()
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
- uses: actions/setup-node@v3
with:
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]
@@ -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,7 +243,7 @@ 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@v4
if: github.event_name != 'pull_request'
@@ -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
+2
View File
@@ -4,6 +4,8 @@ on:
types: [opened,synchronize,reopened,closed]
paths:
- "frontend/**"
merge_group:
jobs:
npm_check:
runs-on: ubuntu-latest
+1 -1
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
+295
View File
@@ -1,6 +1,301 @@
# Changelog
## [1.96.0](https://github.com/windmill-labs/windmill/compare/v1.95.1...v1.96.0) (2023-05-08)
### Features
* add support for full fleged apps (react, svelte, vue) ([#1536](https://github.com/windmill-labs/windmill/issues/1536)) ([13242ab](https://github.com/windmill-labs/windmill/commit/13242abff153b021cac1ecaa3cbf65ae9d87fb69))
* **frontend:** Add a custom deepEqualWithOrderedArray to handle orde… ([#1537](https://github.com/windmill-labs/windmill/issues/1537)) ([3a291f7](https://github.com/windmill-labs/windmill/commit/3a291f7108623b5c7194f0a7f6a3774499669313))
* **frontend:** Add label, description, input style + add displayType… ([#1540](https://github.com/windmill-labs/windmill/issues/1540)) ([bef829d](https://github.com/windmill-labs/windmill/commit/bef829d4805bb6c5330b13dc17c9a89a84ad48ca))
* **frontend:** app modal ([#1518](https://github.com/windmill-labs/windmill/issues/1518)) ([686f5bb](https://github.com/windmill-labs/windmill/commit/686f5bbe1847cb2a92678e7cfdbb51ecf6bbe2b6))
## [1.95.1](https://github.com/windmill-labs/windmill/compare/v1.95.0...v1.95.1) (2023-05-06)
### Bug Fixes
* **cli:** cli flow sync improvements ([e585e3a](https://github.com/windmill-labs/windmill/commit/e585e3aea2b18b6dc0c9fa7ffa1e6c1dfb2a3ce2))
## [1.95.0](https://github.com/windmill-labs/windmill/compare/v1.94.0...v1.95.0) (2023-05-05)
### Features
* **backend:** default parameters are used in python if missing from args ([8791a86](https://github.com/windmill-labs/windmill/commit/8791a86a936301d44ae05ea09d26c9815abf8929))
* **frontend:** App Schema Form component ([#1533](https://github.com/windmill-labs/windmill/issues/1533)) ([85c0d93](https://github.com/windmill-labs/windmill/commit/85c0d939f59411d023cd4b173ce11224d3cbc9db))
* **frontend:** App stepper ([#1529](https://github.com/windmill-labs/windmill/issues/1529)) ([15f1c94](https://github.com/windmill-labs/windmill/commit/15f1c947bb233147f7da261fd32054a51a9c6efa))
* **frontend:** Merge run configuration + triggers ([#1530](https://github.com/windmill-labs/windmill/issues/1530)) ([1be4658](https://github.com/windmill-labs/windmill/commit/1be4658150ef20a9f1f0fe57b5f30ba3c2d4d94e))
## [1.94.0](https://github.com/windmill-labs/windmill/compare/v1.93.1...v1.94.0) (2023-05-04)
### Features
* **frontend:** add eval badge + alert ([#1522](https://github.com/windmill-labs/windmill/issues/1522)) ([32f04c7](https://github.com/windmill-labs/windmill/commit/32f04c796856fa48ddc1548752ba1e7a8802083a))
### Bug Fixes
* **backend:** fix python transformers ([a07e3e8](https://github.com/windmill-labs/windmill/commit/a07e3e84386c0895a7209fc87a4b07218271feca))
* **frontend:** fix ArrayStaticInputEditor width ([#1528](https://github.com/windmill-labs/windmill/issues/1528)) ([b423eec](https://github.com/windmill-labs/windmill/commit/b423eec019785a62c279db01fc93eb3fe08f7f1f))
* **frontend:** fix select width ([#1526](https://github.com/windmill-labs/windmill/issues/1526)) ([f248c09](https://github.com/windmill-labs/windmill/commit/f248c09655889ddace24f451597a56e81443be3c))
* **frontend:** preserve customise arguments ([b4867f1](https://github.com/windmill-labs/windmill/commit/b4867f12bb4f595b5b0e8142ab5d720307ecadd3))
## [1.93.1](https://github.com/windmill-labs/windmill/compare/v1.93.0...v1.93.1) (2023-05-03)
### Bug Fixes
* **cli:** add yaml support for cli ([03e6017](https://github.com/windmill-labs/windmill/commit/03e6017860526784f1a8696eceed5750b25f1c5c))
## [1.93.0](https://github.com/windmill-labs/windmill/compare/v1.92.2...v1.93.0) (2023-05-03)
### Features
* **frontend:** add recompute others to background scripts ([392d0f8](https://github.com/windmill-labs/windmill/commit/392d0f8b876c9b587fe85421098f3eceb8a74dec))
### Bug Fixes
* **frontend:** deploy path for apps ([7ac9677](https://github.com/windmill-labs/windmill/commit/7ac96771a5c3d44234c790e8cea3d621d8c1d00e))
## [1.93.0](https://github.com/windmill-labs/windmill/compare/v1.92.2...v1.93.0) (2023-05-03)
### Features
* **frontend:** add recompute others to background scripts ([392d0f8](https://github.com/windmill-labs/windmill/commit/392d0f8b876c9b587fe85421098f3eceb8a74dec))
### Bug Fixes
* **frontend:** deploy path for apps ([7ac9677](https://github.com/windmill-labs/windmill/commit/7ac96771a5c3d44234c790e8cea3d621d8c1d00e))
## [1.92.2](https://github.com/windmill-labs/windmill/compare/v1.92.1...v1.92.2) (2023-05-02)
### Bug Fixes
* **go-client:** fix go-client gen ([82c4d66](https://github.com/windmill-labs/windmill/commit/82c4d6629e134f00389c87a948c52878e5a3f4f5))
## [1.92.1](https://github.com/windmill-labs/windmill/compare/v1.92.0...v1.92.1) (2023-05-02)
### Bug Fixes
* **go-client:** fix go-client gen ([df333d9](https://github.com/windmill-labs/windmill/commit/df333d9739f601714f7a0124f47422dfb113d320))
## [1.92.0](https://github.com/windmill-labs/windmill/compare/v1.91.0...v1.92.0) (2023-05-02)
### Features
* **frontend:** add labels as table action ([64065c1](https://github.com/windmill-labs/windmill/commit/64065c17f305fb4c7c078c7fa4935d5423da8f66))
* **frontend:** add labels as table action ([2ab1714](https://github.com/windmill-labs/windmill/commit/2ab1714dfa7cffc46b4b6aa40dabdd92c5a6270f))
* **frontend:** allow running eval in every field ([62acbb5](https://github.com/windmill-labs/windmill/commit/62acbb5ab3a0727b306e25a80b74ac8216619501))
* **frontend:** background script can run script and flows ([#1515](https://github.com/windmill-labs/windmill/issues/1515)) ([607c803](https://github.com/windmill-labs/windmill/commit/607c803be91921b53f329a2c2c3c129ce53d6c0c))
### Bug Fixes
* **frontend:** fix small ui issues ([#1513](https://github.com/windmill-labs/windmill/issues/1513)) ([f6ff8ca](https://github.com/windmill-labs/windmill/commit/f6ff8ca232f5725f86a36379956da2731def2580))
## [1.91.0](https://github.com/windmill-labs/windmill/compare/v1.90.0...v1.91.0) (2023-05-01)
### Features
* add drafts for apps ([f7374c8](https://github.com/windmill-labs/windmill/commit/f7374c8204f85b4371e61f34dcd4b66857c0f8ab))
* introduce backend persisted draft systems for scripts ([88e37fe](https://github.com/windmill-labs/windmill/commit/88e37fe0bed58f396690622e925d5e078c60140c))
* introduce draft for flows ([a196642](https://github.com/windmill-labs/windmill/commit/a1966427e893dc8a58c8f2862ded752884843813))
## [1.90.0](https://github.com/windmill-labs/windmill/compare/v1.89.0...v1.90.0) (2023-04-28)
### Features
* **backend:** add EXIT_AFTER_NO_JOB_FOR_SECS for ephemeral workers ([de9abd1](https://github.com/windmill-labs/windmill/commit/de9abd129db13dcdf0e69e2c1e2d3aa558fb783a))
* **backend:** add JOB_RETENTION_SECS to delete completed jobs completed after expiration period ([0b7bad3](https://github.com/windmill-labs/windmill/commit/0b7bad3816e61841ef4765db7881227274c20b23))
* **backend:** expose tag in the job ([#1486](https://github.com/windmill-labs/windmill/issues/1486)) ([324d4f5](https://github.com/windmill-labs/windmill/commit/324d4f5e9e89e6de600e882f23bf545c0b1dc539))
* **frontend:** adapt style ([#1488](https://github.com/windmill-labs/windmill/issues/1488)) ([41a24ec](https://github.com/windmill-labs/windmill/commit/41a24ecd36d9cc537cbd1dd0cd1de6f689be1b8c))
* **frontend:** add an eval input component for flow ([#1494](https://github.com/windmill-labs/windmill/issues/1494)) ([2815f1e](https://github.com/windmill-labs/windmill/commit/2815f1ec71177bb6e89d0d62a8df89030d37b1fc))
* **frontend:** Add new integration icons ([#1479](https://github.com/windmill-labs/windmill/issues/1479)) ([7adacd4](https://github.com/windmill-labs/windmill/commit/7adacd4c9f03d17b47abc515bc391e348a7e6ec1))
* **frontend:** refactor inline script ([#1480](https://github.com/windmill-labs/windmill/issues/1480)) ([05c837e](https://github.com/windmill-labs/windmill/commit/05c837e64f61bfb22ae1f80263deb1c879030985))
* **frontend:** Schedules run now ([#1475](https://github.com/windmill-labs/windmill/issues/1475)) ([47f0f35](https://github.com/windmill-labs/windmill/commit/47f0f35236e02958f6bc00b5652e06e25eabeaf5))
* **frontend:** Small style fix ([#1473](https://github.com/windmill-labs/windmill/issues/1473)) ([7ad496a](https://github.com/windmill-labs/windmill/commit/7ad496ad3f746ffd782856a35c1456999792fa94))
* **frontend:** Support TS union type with a select field ([#1457](https://github.com/windmill-labs/windmill/issues/1457)) ([8b76324](https://github.com/windmill-labs/windmill/commit/8b763249cb1360c122cc81d50a1a95d1ad3ddd5b))
### Bug Fixes
* **frontend:** Allow 0 as select default value ([#1474](https://github.com/windmill-labs/windmill/issues/1474)) ([d8529ff](https://github.com/windmill-labs/windmill/commit/d8529ff3ed6168de60bb24626a3e36ab4beae15c))
* **frontend:** close the modal before deleting a form modal ([#1484](https://github.com/windmill-labs/windmill/issues/1484)) ([430c733](https://github.com/windmill-labs/windmill/commit/430c73399b7e1524ee81d0ce8d7d2eaf16117f9a))
* **frontend:** fix apply connection ([#1487](https://github.com/windmill-labs/windmill/issues/1487)) ([cf59cc0](https://github.com/windmill-labs/windmill/commit/cf59cc04efb853fe06a23042115128689b5d26ee))
* **frontend:** Fix frontend script ([#1476](https://github.com/windmill-labs/windmill/issues/1476)) ([b60a7f6](https://github.com/windmill-labs/windmill/commit/b60a7f63d04b1fc851479060e77613c77d20198a))
* **frontend:** fix recomputa all ([#1491](https://github.com/windmill-labs/windmill/issues/1491)) ([fb05a09](https://github.com/windmill-labs/windmill/commit/fb05a09955f937000fbba0826c03c52e65aa146e))
* **frontend:** Flow editor design updates ([#1477](https://github.com/windmill-labs/windmill/issues/1477)) ([50d814c](https://github.com/windmill-labs/windmill/commit/50d814c3dc55841b16cd1df8ae86d021de4e880c))
* **frontend:** Minor app editor updates ([#1458](https://github.com/windmill-labs/windmill/issues/1458)) ([8fd10b1](https://github.com/windmill-labs/windmill/commit/8fd10b1f5813b3f4200a980de4a85e7d701660a7))
* **frontend:** register applyConnection as a callback to remove unnecessary reactivit ([#1485](https://github.com/windmill-labs/windmill/issues/1485)) ([d915f6b](https://github.com/windmill-labs/windmill/commit/d915f6b004ea7cc27a3c55b3504df902f5db1aef))
* **frontend:** reset ui job loading state when submitting preview job triggers error ([#1483](https://github.com/windmill-labs/windmill/issues/1483)) ([6f8616f](https://github.com/windmill-labs/windmill/commit/6f8616fb273b1f4a1489878a617d239f30ecb1c0))
* **frontend:** Update CLI login request styling ([#1454](https://github.com/windmill-labs/windmill/issues/1454)) ([c77393c](https://github.com/windmill-labs/windmill/commit/c77393c15444868308b44a72bee89e49fc23d80f))
* **frontend:** Update direct exports ([#1456](https://github.com/windmill-labs/windmill/issues/1456)) ([4a2af13](https://github.com/windmill-labs/windmill/commit/4a2af1359ee29001236591f685cacfa9df6715df))
## [1.89.0](https://github.com/windmill-labs/windmill/compare/v1.88.1...v1.89.0) (2023-04-23)
### Features
* **backend:** global cache refactor for pip using tar for each dependency ([#1443](https://github.com/windmill-labs/windmill/issues/1443)) ([369dd0d](https://github.com/windmill-labs/windmill/commit/369dd0dac61e5430856ed9abf7129bbad3b75860))
* **backend:** only run fully deployed scripts ([3d031c7](https://github.com/windmill-labs/windmill/commit/3d031c701705459f418b11d2ca83e71943e4079b))
* **backend:** worker groups ([#1452](https://github.com/windmill-labs/windmill/issues/1452)) ([722783f](https://github.com/windmill-labs/windmill/commit/722783f7f630e3123ffd2605deb21a915188bd20))
* **backend:** workers are instantly ready and sync with global cache in background ([670ba51](https://github.com/windmill-labs/windmill/commit/670ba51d9bd9a9a0b07a2ed064c316234bb5819d))
* **ee:** sync cache in background ([c919827](https://github.com/windmill-labs/windmill/commit/c919827cf8eb9437b6d9bd57b3d3ad883a66de3b))
* **ee:** sync cache in background ([0e77e37](https://github.com/windmill-labs/windmill/commit/0e77e37fbddbb517d5e1b1f07a27a40b63371439))
* **frontend:** Add documentation links ([#1399](https://github.com/windmill-labs/windmill/issues/1399)) ([36acbf7](https://github.com/windmill-labs/windmill/commit/36acbf793b6714dffe4dbb0e2501b9438f034858))
* **frontend:** Add seconds input ([#1445](https://github.com/windmill-labs/windmill/issues/1445)) ([30bf7ad](https://github.com/windmill-labs/windmill/commit/30bf7ad3e9785420b4dcd814ce3c7a444d23cc9f))
* **frontend:** add toast actions ([#1411](https://github.com/windmill-labs/windmill/issues/1411)) ([d173232](https://github.com/windmill-labs/windmill/commit/d17323286a05aa0b1680ef94d7058d2c8902782f))
* **frontend:** reorder array items in app editor ([#1426](https://github.com/windmill-labs/windmill/issues/1426)) ([3615fb2](https://github.com/windmill-labs/windmill/commit/3615fb26fb91d64626d82ace6e6275e424ece832))
* **frontend:** support showing metadata on script add via query param ([#1438](https://github.com/windmill-labs/windmill/issues/1438)) ([3c98452](https://github.com/windmill-labs/windmill/commit/3c98452f50913ef639eba96996f3a6c80508bd63))
### Bug Fixes
* **backend:** avoid potential conflict between pull from tar and background sync ([d76e907](https://github.com/windmill-labs/windmill/commit/d76e90757e209263da7f79fa85052969e7efd63d))
* **backend:** global cache synco only start if all piptars have been downloaded ([5f8a730](https://github.com/windmill-labs/windmill/commit/5f8a730fdfbb9e3d518555f7272a3bb297725f28))
* **frontend:** App color picker overflow issue ([#1449](https://github.com/windmill-labs/windmill/issues/1449)) ([32903d2](https://github.com/windmill-labs/windmill/commit/32903d2839a082d53168bb9177fc88f6ab0ec482))
* **frontend:** fix copy content button width ([#1428](https://github.com/windmill-labs/windmill/issues/1428)) ([d96d4a5](https://github.com/windmill-labs/windmill/commit/d96d4a524edebea65bc602194c8f11f5d69e920a))
* **frontend:** Minor update of app default codes ([#1440](https://github.com/windmill-labs/windmill/issues/1440)) ([fe75aa1](https://github.com/windmill-labs/windmill/commit/fe75aa18f2f27745db35329aa60938694640a8c6))
* **frontend:** Update app default codes ([#1432](https://github.com/windmill-labs/windmill/issues/1432)) ([c8acfbc](https://github.com/windmill-labs/windmill/commit/c8acfbc1ff0f6c23e5a2229ca83a3b09eec826c3))
* **frontend:** Update app mobile preview width ([#1431](https://github.com/windmill-labs/windmill/issues/1431)) ([1764613](https://github.com/windmill-labs/windmill/commit/17646130bcf8cf646a4ccdfa39f9a8791876a137))
* **frontend:** Update flow tooltip z-indexes ([#1433](https://github.com/windmill-labs/windmill/issues/1433)) ([17cb8fc](https://github.com/windmill-labs/windmill/commit/17cb8fc3fa0b39a9750e61ca2731a15bbda690ec))
* **frontend:** Update flow viewer styling ([#1441](https://github.com/windmill-labs/windmill/issues/1441)) ([46a29b5](https://github.com/windmill-labs/windmill/commit/46a29b5d27b8d9f7ea38c1063fc081ed5933db5d))
## [1.88.1](https://github.com/windmill-labs/windmill/compare/v1.88.0...v1.88.1) (2023-04-18)
### Bug Fixes
* **frontend:** fix hub list ([1144329](https://github.com/windmill-labs/windmill/commit/1144329972fb61e2df62873ca1e485c88fabc478))
## [1.88.0](https://github.com/windmill-labs/windmill/compare/v1.87.0...v1.88.0) (2023-04-17)
### Features
* **backend:** install python scripts on save ([cb7e686](https://github.com/windmill-labs/windmill/commit/cb7e686dd95397d5b37edd5aac50b6d1429c4a71))
* **frontend:** Add runs preview popup ([#1405](https://github.com/windmill-labs/windmill/issues/1405)) ([4ab023f](https://github.com/windmill-labs/windmill/commit/4ab023f95085958ab1ad01dc249d308c7ebf423e))
* **frontend:** cancellable inline script editor run ([e828d26](https://github.com/windmill-labs/windmill/commit/e828d2673e62e95e5e1235eeca8107ac7cfb7e45))
* **frontend:** Remove gap when button label is empty ([#1402](https://github.com/windmill-labs/windmill/issues/1402)) ([568f59e](https://github.com/windmill-labs/windmill/commit/568f59eefb104047b8ef063f273fe238075d6407))
* **frontend:** Unify main lists ([#1406](https://github.com/windmill-labs/windmill/issues/1406)) ([48bbbd0](https://github.com/windmill-labs/windmill/commit/48bbbd0e872a12ed1c562a6d14967a2a0f7c4735))
* **frontend:** Update airtable instructions ([#1403](https://github.com/windmill-labs/windmill/issues/1403)) ([7dc7ece](https://github.com/windmill-labs/windmill/commit/7dc7ecef55b465fc096f71fc9de5c8b543136ff7))
* inputs library on run page ([92a2934](https://github.com/windmill-labs/windmill/commit/92a293488e8e58350229931ab69f7924d58474be))
### Bug Fixes
* **backend:** deno uses --no-check ([a5499c2](https://github.com/windmill-labs/windmill/commit/a5499c26f3ebd8b07541a7e0cbf33a7008a8f476))
* **backend:** do not fail on schedule not existing anymore ([a5f6d73](https://github.com/windmill-labs/windmill/commit/a5f6d73f7d53d7af9d285a85460509763263c508))
* **frontend:** Fix app file uploads ([#1408](https://github.com/windmill-labs/windmill/issues/1408)) ([ac489ac](https://github.com/windmill-labs/windmill/commit/ac489ac2da0fbf01f5e2877612c14cfaf1ef79c2))
* **frontend:** fix buttons width ([#1407](https://github.com/windmill-labs/windmill/issues/1407)) ([75a0482](https://github.com/windmill-labs/windmill/commit/75a0482ef046dd7e30f6d6039dbc66880182dc5e))
* **frontend:** fix enum sync ([#1410](https://github.com/windmill-labs/windmill/issues/1410)) ([98060ce](https://github.com/windmill-labs/windmill/commit/98060ce55d5efa59a8989cf9357935976d57650b))
* **frontend:** Handle scheduled runs in preview ([#1413](https://github.com/windmill-labs/windmill/issues/1413)) ([accdc1a](https://github.com/windmill-labs/windmill/commit/accdc1ac59ce9611f66567222a73995d3c0a3f9d))
* **frontend:** Keep selected tab during renaming ([#1409](https://github.com/windmill-labs/windmill/issues/1409)) ([82cd048](https://github.com/windmill-labs/windmill/commit/82cd048ef4d08f31660f6f31a96940676a28996c))
* **frontend:** Queued-running jobs preview ([#1414](https://github.com/windmill-labs/windmill/issues/1414)) ([b2a40a0](https://github.com/windmill-labs/windmill/commit/b2a40a05805344c1c34f2ba917b4cdd52dfffc3f))
* **frontend:** Remove output when deleting a component ([#1397](https://github.com/windmill-labs/windmill/issues/1397)) ([6aa1008](https://github.com/windmill-labs/windmill/commit/6aa100893352870d5a99fdd56d7f1425a221a273))
## [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)
+2 -12
View File
@@ -1,15 +1,5 @@
{
auto_https off
}
http://{$BASE_URL} {
{$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://lsp:3001
}
}
+3 -2
View File
@@ -39,7 +39,7 @@ WORKDIR /windmill
ENV SQLX_OFFLINE=true
ENV CARGO_INCREMENTAL=1
FROM node:19-alpine as frontend
FROM node:20-alpine as frontend
# install dependencies
WORKDIR /frontend
@@ -85,7 +85,7 @@ 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
@@ -124,6 +124,7 @@ ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=frontend /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
+67 -52
View File
@@ -5,7 +5,7 @@
<em>.</em>
</p>
<p align=center>
Open-source developer infrastructure for internal tools. Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs to trigger workflows and scripts as internal apps. Scripts are turned into UIs and no-code modules, no-code modules can be composed into very rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. The script languages supported are: Python, Typescript, Go, Bash, SQL.
Open-source developer infrastructure for internal tools. Self-hostable alternative to Airplane, Pipedream, Superblocks and a simplified Temporal with autogenerated UIsm and custom UIs to trigger workflows and scripts as internal apps. Scripts are turned into UIs and no-code modules, no-code modules can be composed into very rich flows, and script and flows can be triggered from internal UIs made with a low-code builder. The script languages supported are: Python, Typescript, Go, Bash, SQL.
</p>
<p align="center">
@@ -70,10 +70,10 @@ 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
through webhooks.
@@ -123,7 +123,7 @@ instance from local commands. See
### 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
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/>
@@ -186,14 +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://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
curl https://raw.githubusercontent.com/windmill-labs/windmill/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
@@ -283,51 +288,61 @@ it being synced automatically everyday.
## Environment Variables
| Environment Variable name | Default | Description | Api Server/Worker/All |
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| 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 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 |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| 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 |
| Environment Variable name | Default | Description | Api Server/Worker/All |
| ----------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| 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 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 |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
| NETRC | | The netrc content to use a private go registry | Worker |
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| 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 |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails |
| GLOBAL_CACHE_INTERVAL | 10\*60 | (Enterprise Edition only) Interval in seconds in between bucket sync of the cache. This interval \* 2 is the time at which you're guaranteed all the worker's caches are synced together. | Worker |
| WORKER_TAGS | 'deno,go,python3,bash,flow,hub,dependency' | The worker groups assigned to that workers | Worker |
| CUSTOM_TAGS | None | The custom tags assignable to scripts. | Server |
| JOB_RETENTION_SECS | 60*60*24\*60 //60 days | The time in seconds after which jobs get deleted. Set to 0 or -1 to never delete |
| WAIT_RESULT_FAST_POLL_INTERVAL_MS | 50 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_SLOW_POLL_INTERVAL_MS | 200 | The time in between polling for the run_wait_result endpoints in fast poll mode | Server |
| WAIT_RESULT_FAST_POLL_DURATION_SECS | 2 | The duration of fast poll mode before switching to slow poll | Server |
| EXIT_AFTER_NO_JOB_FOR_SECS | None | Exit worker if no job is received after duration in secs if defined | Worker |
## Run a local dev setup
+1 -1
View File
@@ -18,4 +18,4 @@ rustflags = [
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
]
+2 -1
View File
@@ -1,3 +1,4 @@
{
"python.analysis.typeCheckingMode": "basic"
"python.analysis.typeCheckingMode": "basic",
"rust-analyzer.linkedProjects": ["./windmill-common/Cargo.toml"]
}
+698 -252
View File
File diff suppressed because it is too large Load Diff
+20 -10
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.84.0"
version = "1.96.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.84.0"
version = "1.96.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,20 @@ git-version.workspace = true
rsa.workspace = true
base64.workspace = true
sha2.workspace = true
rsmq_async.workspace = true
url.workspace = true
lazy_static.workspace = true
once_cell.workspace = true
prometheus.workspace = true
uuid.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 +80,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 +88,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 }
@@ -99,7 +104,7 @@ rand = "0.8.5"
rand_core = { version = "^0", features = ["std"] }
magic-crypt = "^3"
git-version = "^0"
rustpython-parser = { git = "https://github.com/RustPython/RustPython" }
rustpython-parser = "0.2.0"
cron = "^0"
lettre = { version = "^0", features = [
"rustls-tls",
@@ -142,6 +147,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,4 +159,8 @@ async-stripe = { version = "0.14", features = [
"checkout",
] }
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
once_cell = "1.17.1"
rsmq_async = { version = "5.1.5" }
gosyn = "0.2.2"
bytes = "1.4.0"
+8 -1
View File
@@ -1,6 +1,7 @@
# Windmill Backend
This folder holds all backend components, the [src/](./src/) folder only contains files used to build the "root" binary.
This folder holds all backend components, the [src/](./src/) folder only
contains files used to build the "root" binary.
## Components
@@ -13,3 +14,9 @@ This folder holds all backend components, the [src/](./src/) folder only contain
| [windmill-queue](./windmill-queue/) | Contains job & flow queuing functionality, commonly written to by the API server and read from by workers |
| [windmill-worker](./windmill-worker/) | The worker. Used to process and execute flows & jobs. |
| [parsers](./parsers/) | Contains code to parse signatures in different langauges. |
### Compile sqlx for offline ci
```
cargo sqlx prepare --merged -- --bin windmill --features enterprise
```
@@ -0,0 +1,2 @@
ALTER TABLE schedule DROP COLUMN timezone;
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;
@@ -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_;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -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);
@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE pip_resolution_cache;
@@ -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
);
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS input;
DROP TYPE RUNNABLE_TYPE;
@@ -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
);
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,3 @@
-- Add up migration script here
GRANT ALL ON input TO windmill_user;
GRANT ALL ON input TO windmill_admin;
@@ -0,0 +1,4 @@
-- Add down migration script here
ALTER TABLE script DROP COLUMN tag;
ALTER TABLE completed_job DROP COLUMN tag;
ALTER TABLE queue DROP COLUMN tag;
@@ -0,0 +1,4 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN tag VARCHAR(50);
ALTER TABLE completed_job ADD COLUMN tag VARCHAR(50) NOT NULL DEFAULT 'other';
ALTER TABLE queue ADD COLUMN tag VARCHAR(50) NOT NULL DEFAULT 'other';
@@ -0,0 +1,3 @@
-- Add down migration script here
DROP TABLE draft;
DROP TYPE DRAFT_TYPE;
@@ -0,0 +1,18 @@
-- Add up migration script here
CREATE TYPE DRAFT_TYPE AS ENUM ('script', 'flow', 'app');
CREATE TABLE draft (
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
path VARCHAR(255) NOT NULL,
typ DRAFT_TYPE NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, path, typ)
);
GRANT ALL ON draft TO windmill_admin;
GRANT ALL ON draft TO windmill_user;
ALTER TABLE script ADD COLUMN draft_only BOOLEAN;
ALTER TABLE flow ADD COLUMN draft_only BOOLEAN;
ALTER TABLE app ADD COLUMN draft_only BOOLEAN;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,33 @@
-- Add up migration script here
CREATE TABLE raw_app (
path varchar(255) PRIMARY KEY,
version INTEGER NOT NULL DEFAULT 0,
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
summary VARCHAR(1000) NOT NULL DEFAULT '',
edited_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
data TEXT NOT NULL,
extra_perms JSONB NOT NULL DEFAULT '{}'
);
CREATE POLICY see_own ON raw_app FOR ALL
USING (SPLIT_PART(raw_app.path, '/', 1) = 'u' AND SPLIT_PART(raw_app.path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON raw_app FOR ALL
USING (SPLIT_PART(raw_app.path, '/', 1) = 'g' AND SPLIT_PART(raw_app.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_extra_perms_user ON raw_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 raw_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));
CREATE POLICY see_folder_extra_perms_user ON raw_app FOR ALL
USING (SPLIT_PART(raw_app.path, '/', 1) = 'f' AND SPLIT_PART(raw_app.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
WITH CHECK (SPLIT_PART(raw_app.path, '/', 1) = 'f' AND SPLIT_PART(raw_app.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
ALTER TYPE FAVORITE_KIND ADD VALUE 'raw_app';
@@ -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
@@ -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 {}
@@ -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());
}
}
@@ -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",
}
}
}
+7 -10
View File
@@ -17,8 +17,8 @@ use serde_json::json;
use windmill_common::error;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, Typ};
use rustpython_parser as parser;
use rustpython_parser::ast::{Constant, ExprKind, Located, StmtKind};
use rustpython_parser::parser::parse_program;
const DEF_MAIN: &str = "def main(";
const FUNCTION_CALL: &str = "<function call>";
@@ -64,7 +64,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
"No main function found".to_string(),
));
}
let ast = parser::parse_program(&filtered_code, "main.py").map_err(|e| {
let ast = parse_program(&filtered_code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
})?;
let param = ast.into_iter().find_map(|x| match x {
@@ -134,10 +134,7 @@ fn to_value(et: &ExprKind) -> Option<serde_json::Value> {
.into_iter()
.zip(values)
.map(|(k, v)| {
let key = k
.as_ref()
.map(|x| x.node.clone())
.and_then(|n| to_value(&n))
let key = to_value(&k.node)
.and_then(|x| match x {
serde_json::Value::String(s) => Some(s),
_ => None,
@@ -215,10 +212,10 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
Ok(lines)
} else {
let code = code.split(DEF_MAIN).next().unwrap_or("");
let ast = parser::parse_program(code, "main.py").map_err(|e| {
let ast = 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 {
@@ -250,7 +247,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)
}
}
@@ -460,7 +457,7 @@ def main():
";
let r = parse_python_imports(code)?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib", "requests"]);
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]);
Ok(())
}
+84 -62
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.into());
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,
+980 -553
View File
File diff suppressed because it is too large Load Diff
+5 -3
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()))?;
+108 -22
View File
@@ -6,11 +6,24 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
};
use git_version::git_version;
use monitor::handle_zombie_jobs_periodically;
use sqlx::{Pool, Postgres};
use windmill_common::utils::rd_string;
use tokio::{
fs::{metadata, DirBuilder},
join,
sync::RwLock,
};
use windmill_common::{utils::rd_string, METRICS_ADDR};
use windmill_worker::{
DENO_CACHE_DIR, DENO_TMP_CACHE_DIR, GO_CACHE_DIR, GO_TMP_CACHE_DIR, PIP_CACHE_DIR,
ROOT_TMP_CACHE_DIR, TAR_PIP_TMP_CACHE_DIR,
};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 3;
@@ -18,6 +31,7 @@ const DEFAULT_PORT: u16 = 8000;
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
mod ee;
mod monitor;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@@ -30,15 +44,7 @@ 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")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose()?
.flatten();
let metrics_addr: Option<SocketAddr> = *METRICS_ADDR;
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
.ok()
@@ -57,14 +63,46 @@ 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?;
}
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
let shutdown_signal = windmill_common::shutdown_signal(tx.clone(), rx.resubscribe());
#[cfg(feature = "enterprise")]
tracing::info!(
@@ -104,6 +142,8 @@ Windmill Community Edition {GIT_VERSION}
"PYTHON_PATH",
"DENO_PATH",
"GO_PATH",
"GOPRIVATE",
"NETRC",
"PIP_INDEX_URL",
"PIP_EXTRA_INDEX_URL",
"PIP_TRUSTED_HOST",
@@ -120,16 +160,25 @@ Windmill Community Edition {GIT_VERSION}
"INCLUDE_HEADERS",
"WHITELIST_WORKSPACES",
"BLACKLIST_WORKSPACES",
"NEW_USER_WEBHOOK",
"INSTANCE_EVENTS_WEBHOOK",
"CLOUD_HOSTED",
"GLOBAL_CACHE_INTERVAL",
"WORKER_TAGS",
"CUSTOM_TAGS",
"JOB_RETENTION_SECS",
"WAIT_RESULT_FAST_POLL_DURATION_SECS",
"WAIT_RESULT_SLOW_POLL_INTERVAL_MS",
"WAIT_RESULT_FAST_POLL_INTERVAL_MS",
"EXIT_AFTER_NO_JOB_FOR_SECS",
]);
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<()>
};
@@ -141,24 +190,30 @@ Windmill Community Edition {GIT_VERSION}
rx.resubscribe(),
num_workers,
base_internal_url.clone(),
rsmq.clone(),
)
.await?;
tracing::info!("All workers exited.");
tx.send(())?; // signal server to shutdown
}
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(()),
}
};
@@ -187,10 +242,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();
@@ -198,16 +254,19 @@ 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
join!(
handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq),
windmill_api::delete_expired_items_perdiodically(&db2, rx2)
);
});
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")]
@@ -230,6 +289,28 @@ pub async fn run_workers(
let mut handles = Vec::with_capacity(num_workers as usize);
if metadata(&ROOT_TMP_CACHE_DIR).await.is_ok() {
if let Err(e) = tokio::fs::remove_dir_all(&ROOT_TMP_CACHE_DIR).await {
tracing::info!(error = %e, "Could not remove root tmp cache dir");
}
}
for x in [
PIP_CACHE_DIR,
DENO_CACHE_DIR,
GO_CACHE_DIR,
TAR_PIP_TMP_CACHE_DIR,
DENO_TMP_CACHE_DIR,
GO_TMP_CACHE_DIR,
] {
DirBuilder::new()
.recursive(true)
.create(x)
.await
.expect("could not create initial worker dir");
}
let sync_barrier = Arc::new(RwLock::new(None));
for i in 1..(num_workers + 1) {
let db1 = db.clone();
let instance_name = instance_name.clone();
@@ -237,6 +318,8 @@ 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();
let sync_barrier = sync_barrier.clone();
handles.push(tokio::spawn(monitor.instrument(async move {
tracing::info!(worker = %worker_name, "starting worker");
windmill_worker::run_worker(
@@ -244,9 +327,12 @@ pub async fn run_workers(
&instance_name,
worker_name,
i as u64,
num_workers as u32,
&ip,
rx,
&base_internal_url,
rsmq2,
sync_barrier,
)
.await
})));
+148
View File
@@ -0,0 +1,148 @@
use std::time::Duration;
use once_cell::sync::OnceCell;
use sqlx::{Pool, Postgres};
use tokio::sync::mpsc;
use uuid::Uuid;
use windmill_common::{error, jobs::{JobKind, QueuedJob}, METRICS_ENABLED};
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SESSION_TOKEN_EXPIRY,
};
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "30".to_string());
pub static ref RESTART_ZOMBIE_JOBS: bool = std::env::var("RESTART_ZOMBIE_JOBS")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
static ref QUEUE_ZOMBIE_RESTART_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_restart_count",
"Total number of jobs restarted due to ping timeout."
)
.unwrap();
static ref QUEUE_ZOMBIE_DELETE_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_delete_count",
"Total number of jobs deleted due to their ping timing out in an unrecoverable state."
)
.unwrap();
}
pub async fn handle_zombie_jobs_periodically<
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
>(
db: &Pool<Postgres>,
mut rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
rsmq: Option<R>,
) {
loop {
handle_zombie_jobs(db, base_internal_url, rsmq.clone()).await;
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(30)) => (),
_ = rx.recv() => {
println!("received killpill for monitor job");
break;
}
}
}
}
async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
db: &Pool<Postgres>,
base_internal_url: &str,
rsmq: Option<R>,
) {
if *RESTART_ZOMBIE_JOBS {
let restarted = sqlx::query!(
"UPDATE queue SET running = false, started_at = null, logs = logs || '\nRestarted job after not receiving job''s ping for too long the ' || now() || '\n\n' WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2 AND job_kind != $3 AND same_worker = false RETURNING id, workspace_id, last_ping",
*ZOMBIE_JOB_TIMEOUT,
JobKind::Flow: JobKind,
JobKind::FlowPreview: JobKind,
)
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
if *METRICS_ENABLED {
QUEUE_ZOMBIE_RESTART_COUNT.inc_by(restarted.len() as _);
}
for r in restarted {
tracing::info!(
"restarted zombie job {} {} {}",
r.id,
r.workspace_id,
r.last_ping
);
}
}
let mut timeout_query = "SELECT * FROM queue WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2".to_string();
if *RESTART_ZOMBIE_JOBS {
timeout_query.push_str(" AND same_worker = true");
};
let timeouts = sqlx::query_as::<_, QueuedJob>(&timeout_query)
.bind(ZOMBIE_JOB_TIMEOUT.as_str())
.bind(JobKind::Flow)
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
if *METRICS_ENABLED {
QUEUE_ZOMBIE_DELETE_COUNT.inc_by(timeouts.len() as _);
}
for job in timeouts {
tracing::info!("timedout zombie job {} {}", job.id, job.workspace_id,);
// since the job is unrecoverable, the same worker queue should never be sent anything
let (same_worker_tx_never_used, _same_worker_rx_never_used) = mpsc::channel::<Uuid>(1);
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
"ephemeral-zombie-jobs",
*SESSION_TOKEN_EXPIRY,
&job.email,
)
.await
.expect("could not create job token");
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
token,
workspace: job.workspace_id.to_string(),
client: OnceCell::new(),
};
let last_ping = job.last_ping.clone();
let _ = handle_job_error(
db,
&client,
job,
error::Error::ExecutionErr(format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {})",
last_ping
.map(|x| x.to_string())
.unwrap_or_else(|| "no ping".to_string()),
*ZOMBIE_JOB_TIMEOUT
)),
None,
true,
same_worker_tx_never_used,
"",
base_internal_url,
rsmq.clone(),
)
.await;
}
}
+73 -38
View File
@@ -1,13 +1,19 @@
use std::sync::Arc;
use futures::{stream, Stream};
use serde::Deserialize;
use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
use tokio::sync::RwLock;
use windmill_api::jobs::{CompletedJob, Job};
use windmill_api_client::types::{CreateFlowBody, RawScript};
use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
use windmill_queue::{get_queued_job, JobPayload, RawCode};
use windmill_queue::get_queued_job;
async fn initialize_tracing() {
use std::sync::Once;
@@ -87,7 +93,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 };
}
@@ -127,8 +133,7 @@ mod suspend_resume {
use futures::{Stream, StreamExt};
use serde_json::json;
use sqlx::{query_scalar, types::Uuid};
use windmill_common::flows::FlowValue;
use windmill_queue::JobPayload;
use windmill_common::{flows::FlowValue, jobs::JobPayload};
use super::*;
@@ -252,9 +257,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 +358,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}"
))
@@ -396,7 +397,6 @@ mod retry {
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use windmill_common::flows::FlowValue;
use windmill_queue::JobPayload;
use super::*;
@@ -659,7 +659,7 @@ def main(error, port):
},
}))
.unwrap();
let (attempts, responses) = [
let (_attempts, responses) = [
/* fail the first step twice */
(0x00, None),
(0x00, None),
@@ -681,14 +681,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/tmp/main/step_0.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));
}
}
@@ -824,9 +830,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,
@@ -841,11 +846,11 @@ impl RunJob {
/* running */ false,
None,
true,
None,
)
.await
.expect("push has to succeed");
tx.commit().await.expect("push has to commit");
tx.commit().await.unwrap();
uuid
}
@@ -876,7 +881,7 @@ async fn in_test_worker<Fut: std::future::Future>(
port: u16,
) -> <Fut as std::future::Future>::Output {
let (quit, worker) = spawn_test_worker(db, port);
let worker = tokio::time::timeout(std::time::Duration::from_secs(19), worker);
let worker = tokio::time::timeout(std::time::Duration::from_secs(45), worker);
tokio::pin!(worker);
let res = tokio::select! {
@@ -911,18 +916,20 @@ fn spawn_test_worker(
let db = db.to_owned();
let worker_instance: &str = "test worker instance";
let worker_name: String = next_worker_name();
let i_worker: u64 = Default::default();
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,
i_worker,
1,
1,
ip,
rx,
&base_internal_url,
None,
Arc::new(RwLock::new(None)),
)
.await
};
@@ -1000,6 +1007,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
content: numbers.to_string(),
path: None,
lock: None,
tag: None,
},
stop_after_if: Default::default(),
summary: Default::default(),
@@ -1027,6 +1035,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
content: doubles.to_string(),
path: None,
lock: None,
tag: None,
},
stop_after_if: Default::default(),
summary: Default::default(),
@@ -1127,6 +1136,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
content: write_file.clone(),
path: None,
lock: None,
tag: None,
},
stop_after_if: Default::default(),
summary: Default::default(),
@@ -1165,6 +1175,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
content: write_file,
path: None,
lock: None,
tag: None,
},
stop_after_if: Default::default(),
summary: Default::default(),
@@ -1190,6 +1201,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
.to_string(),
path: None,
lock: None,
tag: None,
},
stop_after_if: Default::default(),
summary: Default::default(),
@@ -1232,6 +1244,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
.to_string(),
path: None,
lock: None,
tag: None,
},
stop_after_if: Default::default(),
summary: Default::default(),
@@ -1518,6 +1531,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) {
@@ -2024,6 +2039,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;
@@ -2059,8 +2084,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/main.ts:1:31)\n at run (file:///tmp/wrapper.ts:9:26)\n at file:///tmp/wrapper.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!({
@@ -2093,8 +2121,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/main.ts:1:31)\n at run (file:///tmp/wrapper.ts:9:26)\n at file:///tmp/wrapper.ts:14:1", "message": "failure"}}, [1, 2]])
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone())
.unwrap()
.error
.name,
"Error"
);
}
@@ -2412,9 +2443,12 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
client
.create_flow(
"test-workspace",
&windmill_api_client::types::OpenFlowWPath {
open_flow: flow,
path: "g/all/flow_lock_all".to_owned(),
&CreateFlowBody {
open_flow_w_path: windmill_api_client::types::OpenFlowWPath {
open_flow: flow,
path: "g/all/flow_lock_all".to_owned(),
},
draft_only: None,
},
)
.await
@@ -2433,20 +2467,21 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
.modules
.into_iter()
.for_each(|m| {
tracing::error!("Module: {:?}", m.value);
assert!(matches!(
m.value,
windmill_api_client::types::FlowModuleValue::Rawscript {
windmill_api_client::types::FlowModuleValue::RawScript(RawScript {
language: windmill_api_client::types::RawScriptLanguage::Deno | windmill_api_client::types::RawScriptLanguage::Bash,
lock: Some(ref lock),
..
} if lock == "")
}) if lock == "")
|| matches!(
m.value,
windmill_api_client::types::FlowModuleValue::Rawscript {
windmill_api_client::types::FlowModuleValue::RawScript(RawScript{
language: windmill_api_client::types::RawScriptLanguage::Go | windmill_api_client::types::RawScriptLanguage::Python3,
lock: Some(ref lock),
..
} if lock.len() > 0)
}) if lock.len() > 0)
);
});
}
+2
View File
@@ -21,5 +21,7 @@ rand.workspace = true
base64.workspace = true
[build-dependencies]
prettyplease = "0.1.25"
progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
serde_json = "1.0"
syn = "1.0"
+3 -1
View File
@@ -13,7 +13,9 @@ fn main() {
let spec = serde_json::from_reader(file).unwrap();
let mut generator = progenitor::Generator::default();
let content = generator.generate_text(&spec).unwrap();
let tokens = generator.generate_tokens(&spec).unwrap();
let ast = syn::parse2(tokens).unwrap();
let content = prettyplease::unparse(&ast);
let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
out_file.push("codegen.rs");
+3
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,4 +70,6 @@ async-stripe = { workspace = true, optional = true }
lazy_static.workspace = true
prometheus.workspace = true
async_zip.workspace = true
rsmq_async.workspace = true
regex.workspace = true
bytes.workspace = true
File diff suppressed because it is too large Load Diff
+141 -17
View File
@@ -9,7 +9,6 @@ use std::collections::HashMap;
*/
use crate::{
db::{UserDB, DB},
jobs::script_path_to_payload,
users::{require_owner_of_path, Authed, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
@@ -32,17 +31,19 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
apps::ListAppQuery,
error::{to_anyhow, Error, JsonResult, Result},
jobs::{script_path_to_payload, JobPayload, RawCode},
users::username_to_permissioned_as,
utils::{
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, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_apps))
.route("/get/p/*path", get(get_app))
.route("/get/draft/*path", get(get_app_w_draft))
.route("/secret_of/*path", get(get_secret_id))
.route("/get/v/*id", get(get_app_by_id))
.route("/exists/*path", get(exists_app))
@@ -74,6 +75,9 @@ pub struct ListableApp {
pub execution_mode: String,
pub starred: bool,
pub edited_at: chrono::DateTime<chrono::Utc>,
pub has_draft: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -98,6 +102,23 @@ pub struct AppWithLastVersion {
pub extra_perms: serde_json::Value,
}
#[derive(Serialize, Deserialize)]
pub struct AppWithLastVersionAndDraft {
pub id: i64,
pub path: String,
pub summary: String,
pub policy: serde_json::Value,
pub versions: Vec<i64>,
pub value: serde_json::Value,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
pub type StaticFields = Map<String, Value>;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -126,6 +147,7 @@ pub struct CreateApp {
pub summary: String,
pub value: serde_json::Value,
pub policy: Policy,
pub draft_only: Option<bool>,
}
#[derive(Deserialize)]
@@ -156,6 +178,8 @@ async fn list_apps(
"app_version.created_at as edited_at",
"app.extra_perms",
"favorite.path IS NOT NULL as starred",
"draft.path IS NOT NULL as has_draft",
"draft_only"
])
.left()
.join("favorite")
@@ -168,6 +192,11 @@ async fn list_apps(
.on(
"app_version.id = versions[array_upper(versions, 1)]"
)
.left()
.join("draft")
.on(
"draft.path = app.path AND draft.workspace_id = app.workspace_id AND draft.typ = 'app'"
)
.order_desc("favorite.path IS NOT NULL")
.order_by("app_version.created_at", true)
.and_where("app.workspace_id = ?".bind(&w_id))
@@ -215,6 +244,37 @@ async fn get_app(
Ok(Json(app))
}
async fn get_app_w_draft(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<AppWithLastVersionAndDraft> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_as!(
AppWithLastVersionAndDraft,
r#"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by,
app.draft_only, draft.value as "draft?"
from app
INNER JOIN app_version ON
app_version.id = app.versions[array_upper(app.versions, 1)]
LEFT JOIN draft ON
app.path = draft.path AND app.workspace_id = draft.workspace_id AND draft.typ = 'app'
WHERE app.path = $1 AND app.workspace_id = $2"#,
path.to_owned(),
&w_id
)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let app = not_found_if_none(app_o, "App", path)?;
Ok(Json(app))
}
async fn get_app_by_id(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -324,14 +384,39 @@ async fn create_app(
return Err(Error::BadRequest("App path cannot be empty".to_string()));
}
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
&app.path,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
&app.path
)));
}
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
&app.path,
&w_id
)
.execute(&mut tx)
.await?;
let id = sqlx::query_scalar!(
"INSERT INTO app
(workspace_id, path, summary, policy, versions)
VALUES ($1, $2, $3, $4, '{}') RETURNING id",
(workspace_id, path, summary, policy, versions, draft_only)
VALUES ($1, $2, $3, $4, '{}', $5) RETURNING id",
w_id,
app.path,
app.summary,
json!(app.policy),
app.draft_only,
)
.fetch_one(&mut tx)
.await?;
@@ -411,6 +496,14 @@ async fn delete_app(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
path,
&w_id
)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM app WHERE path = $1 AND workspace_id = $2",
path,
@@ -441,7 +534,6 @@ async fn update_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
) -> Result<String> {
@@ -456,11 +548,25 @@ async fn update_app(
sqlb.and_where_eq("path", "?".bind(&path));
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
sqlb.set("draft_only", "NULL");
if let Some(npath) = &ns.path {
if npath != path {
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db)
.await?;
require_owner_of_path(&authed, path)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
npath,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
npath
)));
}
}
sqlb.set_str("path", npath);
@@ -521,6 +627,15 @@ async fn update_app(
.execute(&mut tx)
.await?;
}
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
path,
&w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
@@ -565,6 +680,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> {
@@ -583,7 +699,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();
@@ -642,20 +758,27 @@ async fn execute_component(
}
};
let (job_payload, args) = match &payload {
let (job_payload, args, tag) = match &payload {
ExecuteApp { args, raw_code: Some(raw_code), path: None, .. } => {
let content = &raw_code.content;
let payload = JobPayload::Code(raw_code.clone());
let path = digest(content);
let args = build_args(policy, path, args)?;
(payload, args)
(payload, args, None)
}
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?
let (payload, tag) = if path.starts_with("script/") {
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())
(
JobPayload::Flow(path.strip_prefix("flow/").unwrap().to_string()),
None,
)
} else {
return Err(Error::BadRequest(format!(
"path must start with script/ or flow/ (got {})",
@@ -663,7 +786,7 @@ async fn execute_component(
)));
};
let args = build_args(policy, path.to_string(), args)?;
(payload, args)
(payload, args, tag)
}
_ => unreachable!(),
};
@@ -684,10 +807,11 @@ async fn execute_component(
false,
None,
true,
tag,
)
.await?;
tx.commit().await?;
Ok(uuid.to_string())
}
+94
View File
@@ -0,0 +1,94 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2042
* 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, DB},
users::{maybe_refresh_folders, require_owner_of_path, Authed},
};
use axum::{
extract::{Extension, Path},
routing::post,
Json, Router,
};
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use windmill_common::error::Result;
pub fn workspaced_service() -> Router {
Router::new().route("/create", post(create_draft))
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
#[sqlx(type_name = "DRAFT_TYPE", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
pub enum DraftType {
Script,
Flow,
App,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Draft {
pub path: String,
pub value: serde_json::Value,
pub typ: DraftType,
}
async fn create_draft(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(draft): Json<Draft>,
) -> Result<(StatusCode, String)> {
let authed = maybe_refresh_folders(&draft.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
require_owner_of_path(&authed, &draft.path)?;
sqlx::query!(
"INSERT INTO draft
(workspace_id, path, value, typ)
VALUES ($1, $2, $3, $4)
ON CONFLICT (workspace_id, path, typ) DO UPDATE SET value = $3",
&w_id,
draft.path,
draft.value,
draft.typ: DraftType,
)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("draft {} created", draft.path)))
}
// async fn get_draft(
// authed: Authed,
// Extension(user_db): Extension<UserDB>,
// Path((w_id, path)): Path<(String, StripPath)>,
// ) -> JsonResult<Draft> {
// let path = path.to_path();
// let mut tx = user_db.begin(&authed).await?;
// let script_o = sqlx::query_as!(
// Draft,
// r#"SELECT path, value, typ as "typ: DraftType" FROM draft WHERE path = $1 AND workspace_id = $2"#,
// path,
// w_id
// )
// .fetch_optional(&mut tx)
// .await?;
// tx.commit().await?;
// let draft = not_found_if_none(script_o, "draft", path)?;
// Ok(Json(draft))
// }
+1
View File
@@ -29,6 +29,7 @@ pub enum FavoriteKind {
Script,
Flow,
App,
Raw_App,
}
#[derive(Deserialize)]
pub struct Favorite {
+136 -55
View File
@@ -6,34 +6,35 @@
* 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 serde::{Deserialize, Serialize};
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{Postgres, Transaction};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error, JsonResult, Result},
flows::{Flow, ListFlowQuery, ListableFlow, NewFlow},
jobs::JobPayload,
schedule::Schedule,
scripts::Schema,
utils::{
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, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -43,6 +44,7 @@ pub fn workspaced_service() -> Router {
.route("/archive/*path", post(archive_flow_by_path))
.route("/delete/*path", delete(delete_flow_by_path))
.route("/get/*path", get(get_flow_by_path))
.route("/get/draft/*path", get(get_flow_by_path_w_draft))
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
}
@@ -73,6 +75,8 @@ async fn list_flows(
"archived",
"extra_perms",
"favorite.path IS NOT NULL as starred",
"draft.path IS NOT NULL as has_draft",
"draft_only"
])
.left()
.join("favorite")
@@ -80,6 +84,11 @@ async fn list_flows(
"favorite.favorite_kind = 'flow' AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?"
.bind(&authed.username),
)
.left()
.join("draft")
.on(
"draft.path = o.path AND draft.workspace_id = o.workspace_id AND draft.typ = 'flow'"
)
.order_desc("favorite.path IS NOT NULL")
.order_by("edited_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ?".bind(&w_id))
@@ -87,9 +96,8 @@ async fn list_flows(
.limit(per_page)
.clone();
if !lq.show_archived.unwrap_or(false) {
sqlb.and_where_eq("archived", false);
}
sqlb.and_where_eq("archived", lq.show_archived.unwrap_or(false));
if let Some(ps) = &lq.path_start {
sqlb.and_where_like_left("path", "?".bind(ps));
}
@@ -178,20 +186,24 @@ 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, \
schema, dependency_job) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json, NULL)",
schema, dependency_job, draft_only) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json, NULL, $8)",
w_id,
nf.path,
nf.summary,
@@ -199,6 +211,15 @@ async fn create_flow(
nf.value,
&authed.username,
nf.schema.and_then(|x| serde_json::to_string(&x.0).ok()),
nf.draft_only
)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
nf.path,
&w_id
)
.execute(&mut tx)
.await?;
@@ -219,13 +240,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,
@@ -242,8 +261,10 @@ async fn create_flow(
false,
None,
true,
None,
)
.await?;
sqlx::query!(
"UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3",
dependency_job_uuid,
@@ -283,15 +304,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!(
@@ -304,7 +328,7 @@ async fn update_flow(
let old_dep_job = not_found_if_none(old_dep_job, "Flow", flow_path)?;
sqlx::query!(
"UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, \
edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8",
edited_at = now(), schema = $6::text::json, dependency_job = NULL, draft_only = NULL WHERE path = $7 AND workspace_id = $8",
nf.path,
nf.summary,
nf.description,
@@ -318,44 +342,52 @@ 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?;
require_owner_of_path(&authed, flow_path)?;
}
}
let mut schedulables = 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,
flow_path,
w_id,
)
.fetch_all(&mut tx)
.await?;
let schedule = sqlx::query_as!(Schedule,
"UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
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,
flow_path,
w_id,
)
.fetch_optional(&mut tx)
.fetch_all(&mut tx)
.await?;
if let Some(schedule) = schedule {
schedulables.push(schedule);
}
let schedule = sqlx::query_as!(Schedule,
"UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
nf.path,
flow_path,
w_id,
)
.fetch_optional(&mut tx)
.await?;
for schedule in schedulables {
clear_schedule(&mut tx, flow_path, true).await?;
if let Some(schedule) = schedule {
schedulables.push(schedule);
}
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
for schedule in schedulables.into_iter() {
clear_schedule(tx.transaction_mut(), &schedule.path, true).await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
}
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
flow_path,
&w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
@@ -372,7 +404,6 @@ async fn update_flow(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFlow {
@@ -382,7 +413,6 @@ async fn update_flow(
},
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
tx,
&w_id,
@@ -399,6 +429,7 @@ async fn update_flow(
false,
None,
true,
None,
)
.await?;
sqlx::query!(
@@ -441,6 +472,44 @@ async fn get_flow_by_path(
Ok(Json(flow))
}
#[derive(Serialize, sqlx::FromRow)]
pub struct FlowWDraft {
pub path: String,
pub summary: String,
pub description: String,
pub schema: Option<Schema>,
pub value: serde_json::Value,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
async fn get_flow_by_path_w_draft(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<FlowWDraft> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let flow_o = sqlx::query_as::<_, FlowWDraft>(
"SELECT flow.path, flow.summary, flow,description, flow.schema, flow.value, flow.extra_perms, flow.draft_only, draft.value as draft FROM flow
LEFT JOIN draft ON
flow.path = draft.path AND flow.workspace_id = draft.workspace_id AND draft.typ = 'flow'
WHERE flow.path = $1 AND flow.workspace_id = $2",
)
.bind(path)
.bind(w_id)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let flow = not_found_if_none(flow_o, "Flow", path)?;
Ok(Json(flow))
}
async fn exists_flow_by_path(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -459,17 +528,24 @@ async fn exists_flow_by_path(
Ok(Json(exists))
}
#[derive(Deserialize)]
struct Archived {
archived: Option<bool>,
}
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(archived): Json<Archived>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2",
"UPDATE flow SET archived = $1 WHERE path = $2 AND workspace_id = $3",
archived.archived.unwrap_or(true),
path,
&w_id
)
@@ -504,6 +580,14 @@ async fn delete_flow_by_path(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
path,
&w_id
)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM flow WHERE path = $1 AND workspace_id = $2",
path,
@@ -575,6 +659,7 @@ mod tests {
language: scripts::ScriptLang::Deno,
path: None,
lock: None,
tag: None,
},
stop_after_if: Some(StopAfterIf {
expr: "foo = 'bar'".to_string(),
@@ -625,7 +710,6 @@ mod tests {
"modules": [
{
"id": "a",
"input_transforms": {},
"value": {
"input_transforms": {
"test": {
@@ -639,7 +723,6 @@ mod tests {
},
{
"id": "b",
"input_transforms": {},
"value": {
"input_transforms": {},
"type": "rawscript",
@@ -653,7 +736,6 @@ mod tests {
},
{
"id": "c",
"input_transforms": {},
"value": {
"type": "forloopflow",
"iterator": {
@@ -676,7 +758,6 @@ mod tests {
],
"failure_module": {
"id": "d",
"input_transforms": {},
"value": {
"input_transforms": {},
"type": "script",
+31 -51
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::{
@@ -20,13 +22,13 @@ use lazy_static::lazy_static;
use regex::Regex;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error, JsonResult, Result},
error::{self, to_anyhow, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
use serde::{Deserialize, Serialize};
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
use sqlx::{FromRow, Postgres, Transaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -39,7 +41,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/:name", delete(delete_folder))
.route("/addowner/:name", post(add_owner))
.route("/removeowner/:name", post(remove_owner))
.route("/is_owner/*path", get(is_owner))
.route("/is_owner/*path", get(is_owner_api))
}
#[derive(FromRow, Serialize, Deserialize, Clone)]
@@ -144,8 +146,10 @@ lazy_static! {
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> {
@@ -156,8 +160,8 @@ async fn create_folder(
"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()]);
@@ -215,47 +219,29 @@ async fn create_folder(
Ok(format!("Created folder {}", ng.name))
}
pub async fn is_owner(
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, name)): Path<(String, String)>,
pub async fn is_owner_api(
authed: Authed,
Path((_w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
if is_admin {
Ok(Json(true))
Ok(Json(is_owner(&authed, &name)))
}
pub fn is_owner(Authed { is_admin, folders, .. }: &Authed, name: &str) -> bool {
if *is_admin {
true
} else {
Ok(Json(
require_is_owner(&name, &username, &groups, &w_id, &db)
.await
.is_ok(),
))
folders.into_iter().any(|x| x.0 == name && x.2)
}
}
pub async fn require_is_owner(
folder_name: &str,
username: &str,
groups: &Vec<String>,
w_id: &str,
db: &DB,
) -> Result<()> {
let is_owner = query_scalar!(
"SELECT EXISTS(SELECT 1 FROM folder WHERE CONCAT('u/', $1::text) = ANY(owners) AND name = $2 AND workspace_id = $4) OR exists(
SELECT 1 FROM folder, unnest(folder.owners) as o
WHERE o = ANY($3::text[]) AND folder.name = $2 AND folder.workspace_id = $4)",
username,
folder_name,
groups,
w_id,
).fetch_one(db)
.await?
.unwrap_or(false);
if !is_owner {
Err(Error::BadRequest(format!(
"{} is not an owner of {} and hence is not authorized to perform this operation",
username, folder_name
)))
} else {
pub fn require_is_owner(authed: &Authed, name: &str) -> Result<()> {
if is_owner(authed, name) {
Ok(())
} else {
Err(windmill_common::error::Error::NotAuthorized(format!(
"You are not owner of the folder {}",
name
)))
}
}
@@ -487,7 +473,6 @@ async fn delete_folder(
async fn add_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
@@ -496,9 +481,7 @@ async fn add_owner(
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
require_is_owner(&authed, &name)?;
sqlx::query!(
"UPDATE folder SET owners = array_append(owners, $1) WHERE name = $2 AND workspace_id = $3 AND NOT $1 = ANY(owners) RETURNING name",
@@ -534,14 +517,14 @@ pub async fn get_folders_for_user(
username: &str,
groups: &[String],
db: &DB,
) -> Result<Vec<(String, bool)>> {
) -> Result<Vec<(String, bool, bool)>> {
let mut perms = groups
.into_iter()
.map(|x| format!("g/{}", x))
.collect::<Vec<_>>();
perms.insert(0, format!("u/{}", username));
let folders = sqlx::query!(
"SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write FROM folder
"SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write, $1 && owners::text[] as owner FROM folder
WHERE extra_perms ?| $1 AND workspace_id = $2",
&perms[..],
w_id,
@@ -549,7 +532,7 @@ pub async fn get_folders_for_user(
.fetch_all(db)
.await?
.into_iter()
.map(|x| (x.name, x.write.unwrap_or(false)))
.map(|x| (x.name, x.write.unwrap_or(false), x.owner.unwrap_or(false)))
.collect();
Ok(folders)
@@ -557,7 +540,6 @@ pub async fn get_folders_for_user(
async fn remove_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
@@ -566,9 +548,7 @@ async fn remove_owner(
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
require_is_owner(&authed, &name)?;
sqlx::query!(
"UPDATE folder SET owners = array_remove(owners, $1) WHERE name = $2 AND workspace_id = $3 RETURNING name",
+31 -12
View File
@@ -43,25 +43,29 @@ async fn add_granular_acl(
Json(GranularAcl { owner, write }): Json<GranularAcl>,
) -> Result<String> {
let path = path.to_path();
let (kind, path) = path
.split_once('/')
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
let mut tx = user_db.begin(&authed).await?;
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else if kind == "group_" {
} else {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
}
let identifier = if kind == "group_" || kind == "folder" {
"name"
} else {
"path"
};
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&authed, path)?;
} else if kind == "group_" {
crate::groups::require_is_owner(path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else {
require_owner_of_path(&authed, path)?;
}
}
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
true) WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
@@ -86,12 +90,22 @@ async fn remove_granular_acl(
Json(GranularAcl { owner, write: _ }): Json<GranularAcl>,
) -> Result<String> {
let path = path.to_path();
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let (kind, path) = path
.split_once('/')
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&authed, path)?;
} else if kind == "group_" {
crate::groups::require_is_owner(path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else {
require_owner_of_path(&authed, path)?;
}
}
let mut tx = user_db.begin(&authed).await?;
let identifier = if kind == "group_" || kind == "folder" {
@@ -99,6 +113,11 @@ async fn remove_granular_acl(
} else {
"path"
};
if identifier == "path" {
require_owner_of_path(&authed, path)?;
}
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
workspace_id = $3 RETURNING extra_perms"
+6
View File
@@ -292,6 +292,12 @@ async fn delete_group(
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
if name == "all" {
return Err(Error::BadRequest(
"The group 'all' is a special group that contains all users and cannot be deleted".to_string(),
));
}
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
+281
View File
@@ -0,0 +1,281 @@
/*
* 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,
jobs::JobKind,
scripts::to_i64,
utils::{paginate, Pagination},
};
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 * from (select distinct on (args) * from completed_job \
where {} = $1 and job_kind = $2 and workspace_id = $3 \
order by args, started_at desc) t ORDER BY 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()))
}
+214 -69
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},
@@ -25,19 +31,13 @@ use windmill_common::{
error::{self, to_anyhow, Error},
flow_status::{Approval, FlowStatus, FlowStatusModule},
flows::FlowValue,
jobs::{script_path_to_payload, JobKind, JobPayload, QueuedJob, RawCode},
oauth2::HmacSha256,
scripts::{ScriptHash, ScriptLang},
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, OptAuthed},
variables::get_workspace_key,
BASE_URL,
};
use windmill_queue::{get_queued_job, push, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -47,6 +47,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,6 +64,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/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))
@@ -110,6 +115,7 @@ async fn get_result_by_id(
}
async fn cancel_job_api(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
@@ -123,7 +129,7 @@ async fn cancel_job_api(
};
let (mut tx, job_option) =
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, false).await?;
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, false).await?;
if let Some(id) = job_option {
audit_log(
@@ -152,6 +158,7 @@ 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)>,
@@ -165,7 +172,7 @@ async fn force_cancel(
};
let (mut tx, job_option) =
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, true).await?;
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, true).await?;
if let Some(id) = job_option {
audit_log(
@@ -213,6 +220,26 @@ pub async fn get_path_for_hash<'c>(
Ok(path)
}
pub async fn get_path_and_tag_for_hash<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,
hash: i64,
) -> error::Result<(String, Option<String>)> {
let script = sqlx::query!(
"select path, tag from script where hash = $1 AND workspace_id = $2",
hash,
w_id
)
.fetch_one(db)
.await
.map_err(|e| {
Error::InternalErr(format!(
"querying getting path for hash {hash} in {w_id}: {e}"
))
})?;
Ok((script.path, script.tag))
}
async fn get_job(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
@@ -299,6 +326,7 @@ pub struct CompletedJob {
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
pub tag: String,
}
#[derive(Deserialize, Clone)]
@@ -309,6 +337,7 @@ pub struct RunJobQuery {
include_header: Option<String>,
invisible_to_owner: Option<bool>,
queue_limit: Option<i64>,
payload: Option<String>,
}
lazy_static::lazy_static! {
@@ -382,6 +411,7 @@ pub struct ListQueueQuery {
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
pub tag: Option<String>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -404,6 +434,9 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(cb) = &lq.created_by {
sqlb.and_where_eq("created_by", "?".bind(cb));
}
if let Some(t) = &lq.tag {
sqlb.and_where_eq("tag", "?".bind(t));
}
if let Some(r) = &lq.running {
sqlb.and_where_eq("running", &r);
}
@@ -491,6 +524,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>,
@@ -516,6 +569,7 @@ async fn list_jobs(
job_kinds: lq.job_kinds,
suspended: lq.suspended,
args: lq.args,
tag: lq.tag,
},
&[
"'QueuedJob' as typ",
@@ -545,6 +599,7 @@ async fn list_jobs(
"visible_to_owner",
"suspend",
"mem_peak",
"tag",
],
);
let sqlc = list_completed_jobs_query(
@@ -580,6 +635,7 @@ async fn list_jobs(
"visible_to_owner",
"null as suspend",
"mem_peak",
"tag",
],
);
let sql = format!(
@@ -598,7 +654,7 @@ async fn list_jobs(
pub async fn resume_suspended_flow_as_owner(
authed: Authed,
Extension(db): Extension<DB>,
Path((w_id, flow_id)): Path<(String, Uuid)>,
Path((_w_id, flow_id)): Path<(String, Uuid)>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
let value = value.unwrap_or(serde_json::Value::Null);
@@ -606,16 +662,11 @@ pub async fn resume_suspended_flow_as_owner(
let (flow, job_id) = get_suspended_flow_info(flow_id, &mut tx).await?;
if !authed.is_admin {
require_owner_of_path(
&w_id,
&authed.username,
&authed.groups,
&flow.script_path.clone().unwrap_or_else(|| String::new()),
&db,
)
.await?;
}
require_owner_of_path(
&authed,
&flow.script_path.clone().unwrap_or_else(|| String::new()),
)?;
insert_resume_job(0, job_id, &flow, value, Some(authed.username), &mut tx).await?;
resume_immediately_if_relevant(flow, job_id, &mut tx).await?;
@@ -788,6 +839,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> {
@@ -810,6 +862,7 @@ pub async fn cancel_suspended_job(
parent_flow,
&w_id,
tx,
rsmq,
false,
)
.await?;
@@ -1059,6 +1112,7 @@ struct UnifiedJob {
visible_to_owner: bool,
suspend: Option<i32>,
mem_peak: Option<i32>,
tag: String,
}
impl From<UnifiedJob> for Job {
@@ -1094,6 +1148,7 @@ impl From<UnifiedJob> for Job {
email: uj.email,
visible_to_owner: uj.visible_to_owner,
mem_peak: uj.mem_peak,
tag: uj.tag,
}),
"QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id,
@@ -1129,6 +1184,7 @@ impl From<UnifiedJob> for Job {
mem_peak: uj.mem_peak,
root_job: None,
leaf_jobs: None,
tag: uj.tag,
}),
t => panic!("job type {} not valid", t),
}
@@ -1145,6 +1201,7 @@ struct Preview {
path: Option<String>,
args: Option<serde_json::Map<String, serde_json::Value>>,
language: ScriptLang,
tag: Option<String>,
}
#[derive(Deserialize)]
@@ -1194,7 +1251,7 @@ where
}
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
@@ -1203,16 +1260,16 @@ fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D
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(
tx,
&w_id,
@@ -1229,6 +1286,7 @@ pub async fn run_flow_by_path(
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
None,
)
.await?;
tx.commit().await?;
@@ -1238,15 +1296,17 @@ 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, tag) =
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(
@@ -1265,6 +1325,7 @@ pub async fn run_job_by_path(
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
)
.await?;
tx.commit().await?;
@@ -1314,14 +1375,13 @@ async fn run_wait_result<T>(
uuid: Uuid,
Path((w_id, _)): Path<(String, T)>,
) -> error::JsonResult<serde_json::Value> {
let mut result = None;
let iters = if timeout <= 0 {
20
} else if timeout <= 1 {
timeout * 10
let mut result;
let timeout_ms = if timeout <= 0 {
2000
} else {
10 + ((timeout - 1) * 2)
(timeout * 1000) as u64
};
let mut g = Guard {
done: false,
id: uuid,
@@ -1329,7 +1389,10 @@ async fn run_wait_result<T>(
db: user_db.clone(),
authed: authed.clone(),
};
for i in 0..iters {
let fast_poll_duration = *WAIT_RESULT_FAST_POLL_DURATION_SECS as u64 * 1000;
let mut accumulated_delay = 0 as u64;
loop {
let mut tx = user_db.clone().begin(&authed).await?;
result = sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
@@ -1344,7 +1407,16 @@ async fn run_wait_result<T>(
if result.is_some() {
break;
}
let delay = if i < 10 { 100 } else { 500 };
let delay = if accumulated_delay <= fast_poll_duration {
*WAIT_RESULT_FAST_POLL_INTERVAL_MS
} else {
*WAIT_RESULT_SLOW_POLL_INTERVAL_MS
};
accumulated_delay += delay;
if accumulated_delay > timeout_ms {
break;
};
tokio::time::sleep(core::time::Duration::from_millis(delay)).await;
}
if let Some(result) = result {
@@ -1384,10 +1456,80 @@ lazy_static::lazy_static! {
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(20);
pub static ref WAIT_RESULT_FAST_POLL_INTERVAL_MS: u64 = std::env::var("WAIT_RESULT_FAST_POLL_INTERVAL_MS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(50);
pub static ref WAIT_RESULT_FAST_POLL_DURATION_SECS: u16 = std::env::var("WAIT_RESULT_FAST_POLL_DURATION_SECS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(2);
pub static ref WAIT_RESULT_SLOW_POLL_INTERVAL_MS: u64 = std::env::var("WAIT_RESULT_SLOW_POLL_INTERVAL_MS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(200);
}
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, tag) =
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),
tag,
)
.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>,
@@ -1396,9 +1538,9 @@ 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, tag) =
script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
@@ -1410,7 +1552,7 @@ 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,
@@ -1418,6 +1560,7 @@ pub async fn run_wait_result_job_by_path(
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
)
.await?;
tx.commit().await?;
@@ -1435,6 +1578,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>,
@@ -1444,9 +1588,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, tag) = get_path_and_tag_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(
@@ -1457,7 +1601,7 @@ 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,
@@ -1465,6 +1609,7 @@ pub async fn run_wait_result_job_by_hash(
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
)
.await?;
tx.commit().await?;
@@ -1482,6 +1627,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>,
@@ -1491,8 +1637,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(
@@ -1511,9 +1657,9 @@ pub async fn run_wait_result_flow_by_path(
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
None,
)
.await?;
tx.commit().await?;
run_wait_result(
@@ -1526,31 +1672,17 @@ pub async fn run_wait_result_flow_by_path(
.await
}
// a similar function exists on the worker
pub async fn script_path_to_payload<'c>(
script_path: &str,
db: &mut Transaction<'c, Postgres>,
w_id: &String,
) -> std::result::Result<JobPayload, Error> {
let job_payload = if script_path.starts_with("hub/") {
JobPayload::ScriptHub { path: script_path.to_owned() }
} else {
let script_hash = windmill_common::get_latest_hash_for_path(db, w_id, script_path).await?;
JobPayload::ScriptHash { hash: script_hash, path: script_path.to_owned() }
};
Ok(job_payload)
}
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(
@@ -1574,22 +1706,25 @@ async fn run_preview_job(
false,
None,
true,
preview.tag,
)
.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(
@@ -1608,24 +1743,27 @@ async fn run_preview_flow_job(
false,
None,
true,
None,
)
.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, tag) = get_path_and_tag_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(
@@ -1644,9 +1782,11 @@ pub async fn run_job_by_hash(
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
@@ -1733,6 +1873,9 @@ fn list_completed_jobs_query(
if let Some(h) = &lq.script_hash {
sqlb.and_where_eq("script_hash", "?".bind(h));
}
if let Some(t) = &lq.tag {
sqlb.and_where_eq("tag", "?".bind(t));
}
if let Some(cb) = &lq.created_by {
sqlb.and_where_eq("created_by", "?".bind(cb));
}
@@ -1790,6 +1933,7 @@ pub struct ListCompletedQuery {
pub args: Option<String>,
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
pub tag: Option<String>,
}
async fn list_completed_jobs(
@@ -1835,6 +1979,7 @@ async fn list_completed_jobs(
"email",
"visible_to_owner",
"mem_peak",
"tag",
],
)
.sql()?;
+56 -30
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,18 +14,35 @@ 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;
mod capture;
mod db;
mod drafts;
mod favorite;
mod flows;
mod folders;
mod granular_acls;
mod groups;
mod inputs;
pub mod jobs;
mod oauth2;
mod raw_apps;
mod resources;
mod schedule;
mod scripts;
@@ -46,7 +52,7 @@ mod users;
mod utils;
mod variables;
mod webhook_util;
mod worker_ping;
mod workers;
mod workspaces;
pub const GIT_VERSION: &str =
@@ -77,6 +83,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 +103,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,44 +121,57 @@ 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("/raw_apps", raw_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("/drafts", drafts::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(
"/users",
users::global_service().layer(Extension(argon2.clone())),
)
.nest("/workers", worker_ping::global_service())
.nest("/workers", workers::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)),
+21 -18
View File
@@ -29,10 +29,12 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
use tower_cookies::{Cookie, Cookies};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::jobs::JobPayload;
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 +45,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::QueueTransaction;
use std::{fs, str};
@@ -728,6 +730,7 @@ 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)
@@ -740,7 +743,7 @@ async fn slack_command(
}
}
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",
@@ -751,17 +754,20 @@ async fn slack_command(
if let Some(settings) = settings {
if let Some(path) = &settings.slack_command_script {
let payload = if let Some(path) = path.strip_prefix("flow/") {
JobPayload::Flow(path.to_string())
let (payload, tag) = if let Some(path) = path.strip_prefix("flow/") {
(JobPayload::Flow(path.to_string()), None)
} else {
let path = path.strip_prefix("script/").unwrap_or_else(|| path);
let script_hash = windmill_common::get_latest_hash_for_path(
&mut tx,
let (script_hash, tag) = windmill_common::get_latest_deployed_hash_for_path(
tx.transaction_mut(),
&settings.workspace_id,
path,
)
.await?;
JobPayload::ScriptHash { hash: script_hash, path: path.to_owned() }
(
JobPayload::ScriptHash { hash: script_hash, path: path.to_owned() },
tag,
)
};
let mut map = serde_json::Map::new();
map.insert("text".to_string(), serde_json::Value::String(form.text));
@@ -786,16 +792,18 @@ async fn slack_command(
false,
None,
true,
tag,
)
.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?)"
@@ -815,6 +823,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
@@ -849,7 +858,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)) {
@@ -939,14 +949,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 {
+333
View File
@@ -0,0 +1,333 @@
/*
* 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, DB},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
body,
extract::{Extension, Json, Path, Query},
response::Response,
routing::{delete, get, post},
Router,
};
use bytes::Bytes;
use hyper::{header, StatusCode};
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::FromRow;
use std::str;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
apps::ListAppQuery,
error::{Error, JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination, StripPath},
};
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_apps))
.route("/get_data/:version/*path", get(get_data))
.route("/exists/*path", get(exists_app))
.route("/update/*path", post(update_app))
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct ListableApp {
pub path: String,
pub workspace_id: String,
pub summary: String,
pub edited_at: chrono::DateTime<chrono::Utc>,
pub extra_perms: serde_json::Value,
pub starred: bool,
pub version: i32,
}
#[derive(Deserialize)]
pub struct CreateApp {
pub path: String,
pub summary: String,
pub value: String,
}
#[derive(Deserialize)]
pub struct EditApp {
pub path: Option<String>,
pub summary: Option<String>,
pub value: Option<String>,
}
async fn list_apps(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListAppQuery>,
) -> JsonResult<Vec<ListableApp>> {
let (per_page, offset) = paginate(pagination);
let mut sqlb = SqlBuilder::select_from("raw_app as app")
.fields(&[
"app.workspace_id",
"app.path",
"app.edited_at",
"app.summary",
"app.extra_perms",
"app.version",
"favorite.path IS NOT NULL as starred",
])
.left()
.join("favorite")
.on(
"favorite.favorite_kind = 'raw_app' AND favorite.workspace_id = app.workspace_id AND favorite.path = app.path AND favorite.usr = ?"
.bind(&authed.username),
)
.order_desc("favorite.path IS NOT NULL")
.order_by("app.edited_at", true)
.and_where("app.workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
if lq.starred_only.unwrap_or(false) {
sqlb.and_where_is_not_null("favorite.path");
}
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableApp>(&sql)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn get_data(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, _version, path)): Path<(String, u16, StripPath)>,
) -> Result<Response> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let app_o = sqlx::query_scalar!(
"SELECT data FROM raw_app
WHERE path = $1 AND workspace_id = $2",
path.to_owned(),
&w_id
)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let app = not_found_if_none(app_o, "App", path)?;
let res = Response::builder().header(header::CONTENT_TYPE, "text/javascript");
Ok(res
.body(body::boxed(body::Full::from(Bytes::from(app))))
.unwrap())
}
async fn create_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
if &app.path == "" {
return Err(Error::BadRequest("App path cannot be empty".to_string()));
}
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
app.path,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
&app.path
)));
}
sqlx::query!(
"INSERT INTO raw_app
(workspace_id, path, summary, extra_perms, data)
VALUES ($1, $2, $3, '{}', $4)",
w_id,
app.path,
app.summary,
app.value,
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"apps.create",
ActionKind::Create,
&w_id,
Some(&app.path),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path))
}
async fn delete_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"DELETE FROM raw_app WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"apps.delete",
ActionKind::Delete,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
);
Ok(format!("app {} deleted", path))
}
async fn update_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(app): Json<EditApp>,
) -> Result<String> {
use sql_builder::prelude::*;
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let npath = &app.path;
let npath = if npath.is_some() || app.summary.is_some() {
let mut sqlb = SqlBuilder::update_table("raw_app");
sqlb.and_where_eq("path", "?".bind(&path));
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(npath) = &app.path {
if npath != path {
require_owner_of_path(&authed, path)?;
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
npath,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"App with path {} already exists",
npath
)));
}
}
sqlb.set_str("path", npath);
}
if let Some(nsummary) = &app.summary {
sqlb.set_str("summary", nsummary);
}
if let Some(value) = &app.value {
sqlb.set_str("data", value);
sqlb.set("version", "version + 1");
}
sqlb.returning("path");
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
not_found_if_none(npath_o, "Raw App", path)?
} else {
"".to_string()
};
audit_log(
&mut tx,
&authed.username,
"apps.update",
ActionKind::Update,
&w_id,
Some(&npath),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath))
}
async fn exists_app(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
+7 -4
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?;
@@ -382,9 +386,8 @@ async fn update_resource(
if npath != path {
check_path_conflict(&mut tx, &w_id, &npath).await?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
require_owner_of_path(&authed, path)?;
sqlx::query!(
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
+62 -40
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},
utils::{not_found_if_none, paginate, Pagination, StripPath}, jobs::JobKind,
};
use windmill_queue::{self, schedule::push_scheduled_job, JobKind};
use windmill_queue::{self, schedule::push_scheduled_job, 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,
+202 -86
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,17 +21,21 @@ 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},
jobs::JobPayload,
schedule::Schedule,
scripts::{
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash,
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Schema, Script, ScriptHash,
ScriptKind, ScriptLang,
},
users::username_to_permissioned_as,
@@ -42,10 +43,35 @@ 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};
lazy_static::lazy_static! {
pub static ref CUSTOM_TAGS: Vec<String> = std::env::var("CUSTOM_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).filter(|x| !x.is_empty()).collect::<Vec<_>>()).unwrap_or_default();
}
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
#[derive(Serialize, sqlx::FromRow)]
pub struct ScriptWDraft {
pub hash: ScriptHash,
pub path: String,
pub summary: String,
pub description: String,
pub content: String,
pub language: ScriptLang,
pub kind: ScriptKind,
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<serde_json::Value>,
pub schema: Option<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
pub fn global_service() -> Router {
Router::new()
.route(
@@ -60,11 +86,19 @@ 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))
.route("/create", post(create_script))
.route("/archive/p/*path", post(archive_script_by_path))
.route("/get/draft/*path", get(get_script_by_path_w_draft))
.route("/get/p/*path", get(get_script_by_path))
.route("/raw/p/*path", get(raw_script_by_path))
.route("/exists/p/*path", get(exists_script_by_path))
@@ -76,6 +110,7 @@ pub fn workspaced_service() -> Router {
.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>,
@@ -87,22 +122,18 @@ async fn list_scripts(
let mut sqlb = SqlBuilder::select_from("script as o")
.fields(&[
"o.workspace_id",
"hash",
"o.path",
"array_remove(array[parent_hashes[1]], NULL) as parent_hashes",
"summary",
"description",
"created_by",
"created_at",
"COALESCE(draft.created_at, o.created_at) as created_at",
"archived",
"deleted",
"is_template",
"extra_perms",
"CASE WHEN lock_error_logs IS NOT NULL THEN 'error' ELSE null END as lock_error_logs",
"CASE WHEN lock_error_logs IS NOT NULL THEN true ELSE false END as has_deploy_errors",
"language",
"kind",
"favorite.path IS NOT NULL as starred",
"tag",
"draft.path IS NOT NULL as has_draft",
"draft_only"
])
.left()
.join("favorite")
@@ -110,6 +141,11 @@ async fn list_scripts(
"favorite.favorite_kind = 'script' AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?"
.bind(&authed.username),
)
.left()
.join("draft")
.on(
"draft.path = o.path AND draft.workspace_id = o.workspace_id AND draft.typ = 'script'"
)
.order_desc("favorite.path IS NOT NULL")
.order_by("created_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ?".bind(&w_id))
@@ -119,7 +155,7 @@ async fn list_scripts(
if lq.show_archived.unwrap_or(false) {
sqlb.and_where_eq(
"created_at",
"o.created_at",
"(select max(created_at) from script where o.path = path
AND workspace_id = ?)"
.bind(&w_id),
@@ -184,20 +220,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()
{
@@ -223,10 +261,20 @@ async fn create_script(
}
let parent_hashes_and_perms: Option<ParentInfo> = match (&ns.parent_hash, clashing_script) {
(None, None) => Ok(None),
(None, Some(s)) => Err(Error::BadRequest(format!(
(None, Some(s)) if !s.draft_only.unwrap_or(false) => Err(Error::BadRequest(format!(
"Path conflict for {} with non-archived hash {}",
&ns.path, &s.hash
))),
(None, Some(s)) => {
sqlx::query!(
"DELETE FROM script WHERE hash = $1 AND workspace_id = $2",
s.hash.0,
&w_id
)
.execute(&mut tx)
.await?;
Ok(None)
}
(Some(p_hash), o) => {
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
@@ -258,13 +306,10 @@ 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 {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &ps.path, &db)
.await?;
}
require_owner_of_path(&authed, &ps.path)?;
}
let ph = {
@@ -321,8 +366,8 @@ async fn create_script(
//::text::json is to ensure we use serde_json with preserve order
sqlx::query!(
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
content, created_by, schema, is_template, extra_perms, lock, language, kind) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14)",
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16)",
&w_id,
&hash.0,
ns.path,
@@ -337,11 +382,21 @@ async fn create_script(
lock,
ns.language: ScriptLang,
ns.kind.unwrap_or(ScriptKind::Script): ScriptKind,
ns.tag,
ns.draft_only
)
.execute(&mut tx)
.await?;
if let Some(p_path) = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone()) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
p_path,
&w_id
)
.execute(&mut tx)
.await?;
let schedulables = sqlx::query_as!(
Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
@@ -353,43 +408,21 @@ 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?;
}
}
}
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,
None,
false,
false,
None,
true,
)
.await?;
tx
} else {
tx
};
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
ns.path,
&w_id
)
.execute(&mut tx)
.await?;
}
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
audit_log(
@@ -405,7 +438,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(),
},
@@ -430,13 +463,42 @@ 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,
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,
None,
)
.await?;
tx = new_tx;
}
tx.commit().await?;
Ok((StatusCode::CREATED, format!("{}", hash)))
@@ -478,6 +540,31 @@ async fn get_script_by_path(
Ok(Json(script))
}
async fn get_script_by_path_w_draft(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ScriptWDraft> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let script_o = sqlx::query_as::<_, ScriptWDraft>(
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, draft.value as draft FROM script LEFT JOIN draft ON
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
WHERE script.path = $1 AND script.workspace_id = $2 \
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
workspace_id = $2)",
)
.bind(path)
.bind(w_id)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let script = not_found_if_none(script_o, "Script", path)?;
Ok(Json(script))
}
async fn list_paths(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -496,6 +583,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>,
@@ -556,11 +655,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?;
@@ -568,11 +666,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())
})?)?);
@@ -588,11 +685,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",
@@ -618,6 +714,8 @@ async fn archive_script_by_path(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
require_owner_of_path(&authed, path)?;
let hash: i64 = sqlx::query_scalar!(
"UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash",
path,
@@ -731,7 +829,19 @@ async fn delete_script_by_path(
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
require_admin(authed.is_admin, &authed.username)?;
let draft_only = sqlx::query_scalar!(
"SELECT draft_only FROM script WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
if !draft_only {
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,
@@ -761,25 +871,31 @@ async fn delete_script_by_path(
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)
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
enum SigParsing {
Valid(MainArgSignature),
Invalid { error: String },
}
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)
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_bash_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_bash::parse_bash_sig(&code).map(Json)
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))
}
+161 -49
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::{
@@ -76,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))
@@ -102,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(),
@@ -353,7 +359,33 @@ pub struct Authed {
pub username: String,
pub is_admin: bool,
pub groups: Vec<String>,
pub folders: Vec<(String, bool)>,
// (folder name, can write, is owner)
pub folders: Vec<(String, bool, 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]
@@ -478,6 +510,7 @@ pub struct UserInfo {
pub disabled: bool,
pub role: Option<String>,
pub folders: Vec<String>,
pub folders_owners: Vec<String>,
}
#[derive(FromRow, Serialize)]
@@ -564,6 +597,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)]
@@ -776,9 +810,14 @@ async fn whoami(
disabled: false,
role: Some("superadmin".to_string()),
folders: folders
.clone()
.into_iter()
.filter_map(|x| if x.1 { Some(x.0) } else { None })
.collect(),
folders_owners: folders
.into_iter()
.filter_map(|x| if x.2 { Some(x.0) } else { None })
.collect(),
}))
}
}
@@ -864,9 +903,14 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
disabled: usr.disabled,
role: usr.role,
folders: folders
.clone()
.into_iter()
.filter_map(|x| if x.1 { Some(x.0) } else { None })
.collect(),
folders_owners: folders
.into_iter()
.filter_map(|x| if x.2 { Some(x.0) } else { None })
.collect(),
}))
}
@@ -881,47 +925,45 @@ pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<
Ok(groups)
}
pub async fn is_owner_of_path(
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
authed: Authed,
Path((_w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
if is_admin {
if authed.is_admin {
Ok(Json(true))
} else {
Ok(Json(
require_owner_of_path(&w_id, &username, &groups, path, &db)
.await
.is_ok(),
))
Ok(Json(require_owner_of_path(&authed, path).is_ok()))
}
}
pub async fn require_owner_of_path(
w_id: &str,
username: &str,
groups: &Vec<String>,
path: &str,
db: &DB,
) -> Result<()> {
pub fn require_owner_of_path(authed: &Authed, path: &str) -> Result<()> {
if authed.is_admin {
return Ok(());
}
if !path.is_empty() {
let splitted = path.split("/").collect::<Vec<&str>>();
if splitted[0] == "u" {
if splitted[1] == username {
return Ok(());
if splitted[1] == authed.username {
Ok(())
} else {
return Err(Error::BadRequest(format!(
Err(Error::BadRequest(format!(
"only the owner {} is authorized to perform this operation",
splitted[1]
)));
)))
}
} else if splitted[0] == "g" {
return crate::groups::require_is_owner(splitted[1], username, groups, w_id, db).await;
} else if splitted[0] == "f" {
return crate::folders::require_is_owner(splitted[1], username, groups, w_id, db).await;
crate::folders::require_is_owner(authed, splitted[1])
} else {
Err(Error::BadRequest(format!(
"Not recognized path kind: {}",
path
)))
}
} else {
Err(Error::BadRequest(format!(
"Cannot be owner of an empty path"
)))
}
Err(Error::BadRequest(format!("not recognized owner kind")))
}
async fn whois(
@@ -1002,17 +1044,19 @@ async fn decline_invite(
lazy_static! {
pub static ref VALID_USERNAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
pub static ref JOB_RETENTION_SECS: u32 = std::env::var("JOB_RETENTION_SECS")
.ok()
.and_then(|x| x.parse::<u32>().ok())
.unwrap_or(60 * 60 * 24 * 60); // 60 days
}
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"
@@ -1055,6 +1099,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!(
@@ -1295,12 +1344,14 @@ 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(
@@ -1334,19 +1385,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": "global_add"}),
)
.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)))
}
@@ -1592,7 +1633,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'",
@@ -1714,6 +1755,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,
@@ -1814,16 +1907,35 @@ pub async fn delete_expired_items_perdiodically(
Err(e) => tracing::error!("Error deleting token: {}", 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), '*****')",
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
match magic_links_deleted_r {
Ok(tokens) => tracing::debug!("deleted {} tokens: {:?}", tokens.len(), tokens),
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
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()),
}
if *JOB_RETENTION_SECS > 0 {
let deleted_jobs = sqlx::query_scalar!(
"DELETE FROM completed_job WHERE started_at + ((duration_ms/1000 + $1) || ' s')::interval <= now() RETURNING id",
*JOB_RETENTION_SECS as i64
)
.fetch_all(db)
.await;
match deleted_jobs {
Ok(deleted_jobs) => {
tracing::info!(
"deleted {} jobs completed JOB_RETENTION_SECS ago: {:?}",
deleted_jobs.len(),
deleted_jobs
)
}
Err(e) => tracing::error!("Error deleting jobs: {}", e.to_string()),
}
}
tokio::select! {
+7 -11
View File
@@ -9,16 +9,9 @@
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},
};
/*
* 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 axum::{
extract::{Extension, Path, Query},
@@ -206,12 +199,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?;
@@ -330,6 +326,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?;
@@ -376,9 +373,8 @@ async fn update_variable(
if let Some(npath) = ns.path {
if npath != path {
check_path_conflict(&mut tx, &w_id, &npath).await?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
require_owner_of_path(&authed, path)?;
let mut v = sqlx::query_scalar!(
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
path,
+44 -6
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)]
@@ -45,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
@@ -65,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
@@ -89,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)) => {
@@ -109,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));
}
}
@@ -21,7 +21,16 @@ use windmill_common::{
};
pub fn global_service() -> Router {
Router::new().route("/list", get(list_worker_pings))
Router::new()
.route("/list", get(list_worker_pings))
.route("/custom_tags", get(get_custom_tags))
}
lazy_static::lazy_static! {
pub static ref CUSTOM_TAGS: Vec<String> = std::env::var("CUSTOM_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<_>>()).unwrap_or_default();
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -54,3 +63,7 @@ async fn list_worker_pings(
tx.commit().await?;
Ok(Json(rows))
}
async fn get_custom_tags() -> Json<Vec<String>> {
Json(CUSTOM_TAGS.clone())
}
+19 -20
View File
@@ -16,10 +16,10 @@ use crate::{
db::{UserDB, DB},
folders::Folder,
resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK, VALID_USERNAME},
users::{Authed, WorkspaceInvite, VALID_USERNAME},
utils::require_super_admin,
variables::build_crypt,
HTTP_CLIENT,
webhook_util::{InstanceEvent, WebhookShared},
};
#[cfg(feature = "enterprise")]
use axum::response::Redirect;
@@ -943,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!(
@@ -964,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": "workspace_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,
@@ -982,10 +981,12 @@ 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) {
@@ -1019,14 +1020,10 @@ async fn add_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": "workspace_add"}))
.send()
.await
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
}
webhook.send_instance_event(InstanceEvent::UserAddedWorkspace {
workspace: w_id.clone(),
email: nu.email.clone(),
});
Ok((
StatusCode::CREATED,
@@ -1168,6 +1165,8 @@ where
"edited_at",
"edited_by",
"archived",
"has_draft",
"draft_only",
] {
if obj.contains_key(key) {
obj.remove(key);
+2 -2
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,
+7 -1
View File
@@ -11,10 +11,16 @@
//! For users writing scripts to access their infrastructure with firewalls requiring incoming
//! connections to be from whitelisted IP addresses.
use std::time::Duration;
use reqwest::Result;
pub async fn get_ip() -> Result<String> {
reqwest::get("https://hub.windmill.dev/getip")
reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(3))
.build()?
.get("https://hub.windmill.dev/getip")
.send()
.await?
.error_for_status()?
.text()
+8
View File
@@ -33,6 +33,8 @@ pub struct Flow {
pub archived: bool,
pub schema: Option<Schema>,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
#[derive(Serialize)]
@@ -47,6 +49,9 @@ pub struct ListableFlow {
pub archived: bool,
pub extra_perms: serde_json::Value,
pub starred: bool,
pub has_draft: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
#[derive(Deserialize)]
@@ -57,6 +62,7 @@ pub struct NewFlow {
pub description: String,
pub value: serde_json::Value,
pub schema: Option<Schema>,
pub draft_only: Option<bool>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
@@ -252,6 +258,8 @@ pub enum FlowModuleValue {
lock: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
language: ScriptLang,
},
Identity,
+200
View File
@@ -1 +1,201 @@
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
use uuid::Uuid;
use crate::{
error,
flow_status::FlowStatus,
flows::FlowValue,
get_latest_deployed_hash_for_path,
scripts::{ScriptHash, ScriptLang},
};
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
#[sqlx(type_name = "JOB_KIND", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase"))]
pub enum JobKind {
Script,
#[allow(non_camel_case_types)]
Script_Hub,
Preview,
Dependencies,
Flow,
FlowPreview,
Identity,
FlowDependencies,
}
#[derive(Debug, sqlx::FromRow, Serialize, Clone)]
pub struct QueuedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_lock: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_ping: Option<chrono::DateTime<chrono::Utc>>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub same_worker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_run_error: Option<String>,
pub email: String,
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
#[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>,
pub tag: String,
}
impl QueuedJob {
pub fn script_path(&self) -> &str {
self.script_path
.as_ref()
.map(String::as_str)
.unwrap_or("tmp/main")
}
}
impl QueuedJob {
pub fn parse_raw_flow(&self) -> Option<FlowValue> {
self.raw_flow
.as_ref()
.and_then(|v| serde_json::from_value::<FlowValue>(v.clone()).ok())
}
pub fn parse_flow_status(&self) -> Option<FlowStatus> {
self.flow_status
.as_ref()
.and_then(|v| serde_json::from_value::<FlowStatus>(v.clone()).ok())
}
}
impl Default for QueuedJob {
fn default() -> Self {
Self {
workspace_id: "".to_string(),
id: Uuid::default(),
parent_job: None,
created_by: "".to_string(),
created_at: chrono::Utc::now(),
started_at: None,
scheduled_for: chrono::Utc::now(),
running: false,
script_hash: None,
script_path: None,
args: None,
logs: None,
raw_code: None,
raw_lock: None,
canceled: false,
canceled_by: None,
canceled_reason: None,
last_ping: None,
job_kind: JobKind::Identity,
schedule_path: None,
permissioned_as: "".to_string(),
flow_status: None,
raw_flow: None,
is_flow_step: false,
language: None,
same_worker: false,
pre_run_error: None,
email: "".to_string(),
visible_to_owner: false,
suspend: None,
mem_peak: None,
root_job: None,
leaf_jobs: None,
tag: "deno".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub enum JobPayload {
ScriptHub { path: String },
ScriptHash { hash: ScriptHash, path: String },
Code(RawCode),
Dependencies { hash: ScriptHash, dependencies: String, language: ScriptLang },
FlowDependencies { path: String },
Flow(String),
RawFlow { value: FlowValue, path: Option<String> },
Identity,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RawCode {
pub content: String,
pub path: Option<String>,
pub language: ScriptLang,
pub lock: Option<String>,
}
type Tag = String;
pub async fn script_path_to_payload<'c>(
script_path: &str,
db: &mut Transaction<'c, Postgres>,
w_id: &String,
) -> error::Result<(JobPayload, Option<Tag>)> {
let (job_payload, tag) = if script_path.starts_with("hub/") {
(JobPayload::ScriptHub { path: script_path.to_owned() }, None)
} else {
let (script_hash, tag) = get_latest_deployed_hash_for_path(db, w_id, script_path).await?;
(
JobPayload::ScriptHash { hash: script_hash, path: script_path.to_owned() },
tag,
)
};
Ok((job_payload, tag))
}
pub async fn script_hash_to_tag<'c>(
script_hash: &ScriptHash,
db: &mut Transaction<'c, Postgres>,
w_id: &String,
) -> error::Result<Option<Tag>> {
Ok(sqlx::query_scalar!(
"select tag from script where hash = $1 AND workspace_id = $2",
script_hash.0,
w_id
)
.fetch_optional(db)
.await?
.flatten())
}
+72 -15
View File
@@ -15,6 +15,7 @@ pub mod error;
pub mod external_ip;
pub mod flow_status;
pub mod flows;
pub mod jobs;
pub mod more_serde;
pub mod oauth2;
pub mod schedule;
@@ -30,11 +31,26 @@ 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: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
}
#[cfg(feature = "tokio")]
pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::Result<()> {
pub async fn shutdown_signal(
tx: tokio::sync::broadcast::Sender<()>,
mut rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> {
use std::io;
use tokio::signal::unix::SignalKind;
@@ -48,6 +64,9 @@ pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::
tokio::select! {
_ = terminate() => {},
_ = tokio::signal::ctrl_c() => {},
_ = rx.recv() => {
tracing::info!("shutdown monitor received killpill");
},
}
println!("signal received, starting graceful shutdown");
let _ = tx.send(());
@@ -58,14 +77,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");
@@ -117,23 +153,44 @@ pub async fn connect(
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
}
// TODO: Move this elsewhere
pub async fn get_latest_hash_for_path<'c>(
type Tag = String;
pub async fn get_latest_deployed_hash_for_path<'c>(
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
w_id: &str,
script_path: &str,
) -> error::Result<scripts::ScriptHash> {
let script_hash_o = sqlx::query_scalar!(
"select hash from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2) AND
deleted = false",
) -> error::Result<(scripts::ScriptHash, Option<Tag>)> {
let r_o = sqlx::query!(
"select hash, tag from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND archived = false AND lock IS not NULL AND lock_error_logs IS NULL)",
script_path,
w_id
)
.fetch_optional(db)
.await?;
let script_hash = utils::not_found_if_none(script_hash_o, "ScriptHash", script_path)?;
let script = utils::not_found_if_none(r_o, "script", script_path)?;
Ok(scripts::ScriptHash(script_hash))
Ok((scripts::ScriptHash(script.hash), script.tag))
}
pub async fn get_latest_hash_for_path<'c>(
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
w_id: &str,
script_path: &str,
) -> error::Result<(scripts::ScriptHash, Option<Tag>)> {
let r_o = sqlx::query!(
"select hash, tag from script where path = $1 AND workspace_id = $2 AND
created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND workspace_id = $2 AND
deleted = false AND archived = false)",
script_path,
w_id
)
.fetch_optional(db)
.await?;
let script = utils::not_found_if_none(r_o, "script", script_path)?;
Ok((scripts::ScriptHash(script.hash), script.tag))
}
+1 -1
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,
+12 -9
View File
@@ -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",
@@ -136,27 +136,28 @@ pub struct Script {
pub lock_error_logs: Option<String>,
pub language: ScriptLang,
pub kind: ScriptKind,
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
#[derive(Serialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct ListableScript {
pub workspace_id: String,
pub hash: ScriptHash,
pub path: String,
pub parent_hashes: Option<ScriptHashes>,
pub summary: String,
pub description: String,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub archived: bool,
pub deleted: bool,
pub is_template: bool,
pub extra_perms: serde_json::Value,
pub lock_error_logs: Option<String>,
pub language: ScriptLang,
pub kind: ScriptKind,
pub starred: bool,
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_draft: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
pub has_deploy_errors: bool,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -186,6 +187,8 @@ pub struct NewScript {
pub lock: Option<Vec<String>>,
pub language: ScriptLang,
pub kind: Option<ScriptKind>,
pub tag: Option<String>,
pub draft_only: Option<bool>,
}
#[derive(Deserialize)]
+13 -6
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())
}
+5
View File
@@ -25,8 +25,13 @@ 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
itertools.workspace = true
+133 -147
View File
@@ -9,8 +9,8 @@
use std::collections::HashMap;
use anyhow::Context;
use itertools::Itertools;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, Transaction};
use tracing::{instrument, Instrument};
use ulid::Ulid;
@@ -20,10 +20,14 @@ use windmill_common::{
error::{self, Error},
flow_status::{FlowStatus, JobResult, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL},
flows::{FlowModule, FlowModuleValue, FlowValue},
jobs::{JobKind, JobPayload, QueuedJob, RawCode},
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")
@@ -47,17 +51,34 @@ lazy_static::lazy_static! {
.unwrap();
pub static ref CLOUD_HOSTED: bool = std::env::var("CLOUD_HOSTED").is_ok();
pub static ref ACCEPTED_TAGS: Vec<String> = std::env::var("WORKER_TAGS")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect())
.unwrap_or_else(|| vec![
"deno".to_string(),
"python3".to_string(),
"go".to_string(),
"bash".to_string(),
"dependency".to_string(),
"flow".to_string(),
"hub".to_string(),
"other".to_string()]);
pub static ref ACCEPTED_TAGS_FILTER: String = format!(" AND ({})",
ACCEPTED_TAGS.clone().into_iter().map(|x| format!("(tag = '{x}')")).join(" OR "));
}
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!(
@@ -71,6 +92,12 @@ pub async fn cancel_job<'c>(
)
.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();
@@ -90,10 +117,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 {
@@ -105,6 +133,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!(
@@ -115,16 +146,51 @@ 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 {
let accepted_tags_filter = &*ACCEPTED_TAGS_FILTER;
/* 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()
@@ -136,17 +202,20 @@ pub async fn pull(
AND scheduled_for <= now())
OR (suspend_until IS NOT NULL
AND ( suspend <= 0
OR suspend_until <= now()))) {workspaces_filter}
OR suspend_until <= now())))
{workspaces_filter}
{accepted_tags_filter}
ORDER BY scheduled_for
FOR UPDATE SKIP LOCKED
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();
}
@@ -206,24 +275,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>(
@@ -242,9 +313,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>,
@@ -259,8 +330,8 @@ pub async fn push<'c>(
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);
mut tag: Option<String>,
) -> Result<(Uuid, QueueTransaction<'c, R>), Error> {
let args_json = serde_json::Value::Object(args);
let job_id: Uuid = Ulid::new().into();
@@ -507,12 +578,34 @@ pub async fn push<'c>(
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
let tag = if job_kind == JobKind::Dependencies || job_kind == JobKind::FlowDependencies {
"dependency".to_string()
} else if job_kind == JobKind::Flow || job_kind == JobKind::FlowPreview {
"flow".to_string()
} else if job_kind == JobKind::Identity {
// identity is a light script, deno is too
"deno".to_string()
} else if job_kind == JobKind::Script_Hub {
"hub".to_string()
} else {
if tag == Some("".to_string()) {
tag = None;
}
tag.unwrap_or_else(|| {
language
.as_ref()
.map(|x| x.as_str())
.unwrap_or_else(|| "deno")
.to_string()
})
};
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, root_job)
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, $23) \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job, tag)
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, $24) \
RETURNING id",
workspace_id,
job_id,
@@ -520,7 +613,7 @@ pub async fn push<'c>(
parent_job,
user,
permissioned_as,
scheduled_for,
scheduled_for_o,
script_hash,
script_path.clone(),
raw_code,
@@ -536,13 +629,16 @@ pub async fn push<'c>(
pre_run_error.map(|e| e.to_string()),
email,
visible_to_owner,
root_job
root_job,
tag
)
.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();
@@ -577,6 +673,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))
}
@@ -598,117 +698,3 @@ pub async fn get_hub_script(
.await
.map(|e| e)
}
#[derive(Debug, sqlx::FromRow, Serialize, Clone)]
pub struct QueuedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_lock: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_ping: Option<chrono::DateTime<chrono::Utc>>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub same_worker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_run_error: Option<String>,
pub email: String,
pub visible_to_owner: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
#[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 {
pub fn script_path(&self) -> &str {
self.script_path
.as_ref()
.map(String::as_str)
.unwrap_or("tmp/main")
}
}
impl QueuedJob {
pub fn parse_raw_flow(&self) -> Option<FlowValue> {
self.raw_flow
.as_ref()
.and_then(|v| serde_json::from_value::<FlowValue>(v.clone()).ok())
}
pub fn parse_flow_status(&self) -> Option<FlowStatus> {
self.flow_status
.as_ref()
.and_then(|v| serde_json::from_value::<FlowStatus>(v.clone()).ok())
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
#[sqlx(type_name = "JOB_KIND", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase"))]
pub enum JobKind {
Script,
#[allow(non_camel_case_types)]
Script_Hub,
Preview,
Dependencies,
Flow,
FlowPreview,
Identity,
FlowDependencies,
}
#[derive(Debug, Clone)]
pub enum JobPayload {
ScriptHub { path: String },
ScriptHash { hash: ScriptHash, path: String },
Code(RawCode),
Dependencies { hash: ScriptHash, dependencies: String, language: ScriptLang },
FlowDependencies { path: String },
Flow(String),
RawFlow { value: FlowValue, path: Option<String> },
Identity,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RawCode {
pub content: String,
pub path: Option<String>,
pub language: ScriptLang,
pub lock: Option<String>,
}
+2
View File
@@ -7,6 +7,8 @@
*/
mod jobs;
mod queue_transaction;
pub mod schedule;
pub use jobs::*;
pub use queue_transaction::*;
@@ -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)
}
}
+43 -33
View File
@@ -6,10 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::str::FromStr;
use chrono::Duration;
use crate::push;
use crate::QueueTransaction;
use sqlx::{query_scalar, Postgres, Transaction};
use std::str::FromStr;
use windmill_common::jobs::JobPayload;
use windmill_common::{
error::{self, Result},
schedule::Schedule,
@@ -17,22 +18,28 @@ use windmill_common::{
utils::{now_from_db, StripPath},
};
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)",
@@ -60,21 +67,30 @@ pub async fn push_scheduled_job<'c>(
}
}
let payload = if schedule.is_flow {
JobPayload::Flow(schedule.script_path)
let (payload, tag) = if schedule.is_flow {
(JobPayload::Flow(schedule.script_path), None)
} else {
JobPayload::ScriptHash {
hash: windmill_common::get_latest_hash_for_path(
&mut tx,
&schedule.workspace_id,
&schedule.script_path,
)
.await?,
path: schedule.script_path,
}
let (hash, tag) = windmill_common::get_latest_hash_for_path(
tx.transaction_mut(),
&schedule.workspace_id,
&schedule.script_path,
)
.await?;
(
JobPayload::ScriptHash { hash, path: schedule.script_path },
tag,
)
};
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,
@@ -90,16 +106,10 @@ pub async fn push_scheduled_job<'c>(
false,
None,
true,
tag,
)
.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>(
+2 -1
View File
@@ -48,4 +48,5 @@ deno_core.workspace = true
const_format.workspace = true
git-version.workspace = true
dyn-iter.workspace = true
once_cell.workspace = true
once_cell.workspace = true
rsmq_async.workspace = true
@@ -1,129 +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}/wrapper.ts"
dst: "/tmp/wrapper.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}/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"
+41
View File
@@ -0,0 +1,41 @@
use sqlx::{Pool, Postgres};
use tokio::{fs::File, io::AsyncReadExt};
use windmill_common::error::{self, Error};
use windmill_queue::CLOUD_HOSTED;
use crate::MAX_RESULT_SIZE;
pub async fn read_result(job_dir: &str) -> error::Result<serde_json::Value> {
let mut file = File::open(format!("{job_dir}/result.json")).await?;
let mut content = "".to_string();
file.read_to_string(&mut content).await?;
if *CLOUD_HOSTED && content.len() > MAX_RESULT_SIZE {
return Err(Error::ExecutionErr("Result is too large for the cloud app (limit 2MB).
If using this script as part of the flow, use the shared folder to pass heavy data between steps.".to_owned()));
}
serde_json::from_str(&content)
.map_err(|e| Error::ExecutionErr(format!("Error parsing result: {e}")))
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn set_logs(logs: &str, id: &uuid::Uuid, db: &Pool<Postgres>) {
if sqlx::query!(
"UPDATE queue SET logs = $1 WHERE id = $2",
logs.to_owned(),
id
)
.execute(db)
.await
.is_err()
{
tracing::error!(%id, "error updating logs for id {id}")
};
}
pub fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
+521
View File
@@ -0,0 +1,521 @@
#[cfg(feature = "enterprise")]
use crate::{ROOT_CACHE_DIR, ROOT_TMP_CACHE_DIR, TAR_CACHE_RATE, TAR_PIP_TMP_CACHE_DIR, TMP_DIR};
#[cfg(feature = "enterprise")]
use itertools::Itertools;
#[cfg(feature = "enterprise")]
use rand::Rng;
#[cfg(feature = "enterprise")]
use std::process::Stdio;
#[cfg(feature = "enterprise")]
use tokio::{process::Command, sync::mpsc::Sender, time::Instant};
#[cfg(feature = "enterprise")]
use windmill_common::error;
#[cfg(feature = "enterprise")]
const TAR_CACHE_FILENAME: &str = "denogocache.tar";
#[cfg(feature = "enterprise")]
pub async fn build_tar_and_push(bucket: &str, folder: String) -> error::Result<()> {
tracing::info!("Started building and pushing piptar {folder}");
let start = Instant::now();
let folder_name = folder.split("/").last().unwrap();
let tar_path = format!("{TAR_PIP_TMP_CACHE_DIR}/{folder_name}.tar",);
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"tar",
vec!["-c", "-f", &tar_path, "-C", &folder, "."],
)
.await
{
tracing::info!("Failed to tar cache. Error: {:?}", e);
return Err(e);
}
let tar_metadata = tokio::fs::metadata(&tar_path).await;
if tar_metadata.is_err() || tar_metadata.as_ref().unwrap().len() == 0 {
tracing::info!("Failed to tar cache: {folder}");
return Err(error::Error::ExecutionErr(format!(
"Failed to tar cache: {folder}"
)));
}
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"rclone",
vec![
"copyto",
&tar_path,
&format!(":s3,env_auth=true:{bucket}/tar/pip/{folder_name}.tar"),
"-v",
"--size-only",
"--fast-list",
"--s3-no-check-bucket",
],
)
.await
{
tracing::info!("Failed to copy piptar {folder} to bucket. Error: {:?}", e);
return Err(e);
}
tracing::info!(
"Finished copying piptar {folder} to bucket {bucket} as tar, took: {:?}s. Size of tar: {}",
start.elapsed().as_secs(),
tar_metadata.unwrap().len()
);
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn pull_from_tar(bucket: &str, folder: String) -> error::Result<()> {
use tokio::fs::metadata;
let folder_name = folder.split("/").last().unwrap();
tracing::info!("Attempting to pull piptar {folder_name} from bucket");
let start = Instant::now();
let tar_path = format!("tar/pip/{folder_name}.tar");
let target = format!("{ROOT_TMP_CACHE_DIR}/{tar_path}.single");
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"rclone",
vec![
"copyto",
&format!(":s3,env_auth=true:{bucket}/{tar_path}"),
&target,
"-v",
"--size-only",
"--fast-list",
],
)
.await
{
tracing::info!(
"Failed to copy tar {folder_name} from bucket. Error: {:?}",
e
);
return Err(e);
}
if metadata(&target).await.is_err() {
tracing::info!(
"piptar {folder_name} not found in bucket. Took {:?}ms",
start.elapsed().as_millis()
);
return Err(error::Error::ExecutionErr(format!(
"tar {folder_name} does not exist in bucket"
)));
}
extract_pip_tar(&target, &folder).await?;
tokio::fs::remove_file(&target).await?;
tracing::info!(
"Finished pulling and extracting {folder_name}. Took {:?}ms",
start.elapsed().as_millis()
);
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn cache_global(bucket: &str, tx: Sender<()>) -> error::Result<()> {
copy_cache_from_bucket(bucket, tx).await?;
copy_cache_to_bucket(bucket).await?;
// this is to prevent excessive tar upload. 1/100*15min = each worker sync its tar once per day on average
if rand::thread_rng().gen_range(0..*TAR_CACHE_RATE) == 0 {
copy_cache_to_bucket_as_tar(bucket).await;
}
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_from_bucket(bucket: &str, tx: Sender<()>) -> error::Result<()> {
tracing::info!("Copying cache from bucket in the background {bucket}");
let bucket = bucket.to_string();
let start = Instant::now();
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"rclone",
vec![
"copy",
&format!(":s3,env_auth=true:{bucket}"),
&ROOT_TMP_CACHE_DIR,
"--size-only",
"--fast-list",
"--filter",
"- deno/gen/file/**",
"--filter",
"+ deno/**",
"--filter",
"+ go/**",
"--filter",
"+ tar/**",
"--filter",
"- *",
],
)
.await
{
tracing::info!("Failed to copy cache from bucket. Error: {:?}", e);
return Err(e);
}
tracing::info!(
"Finished copying cache from bucket {bucket}, took {:?}s",
start.elapsed().as_secs()
);
tx.send(()).await.expect("can send copy cache signal");
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_to_bucket(bucket: &str) -> error::Result<()> {
tracing::info!("Copying cache to bucket {bucket}");
let start = Instant::now();
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"rclone",
vec![
"copy",
&ROOT_TMP_CACHE_DIR,
&format!(":s3,env_auth=true:{bucket}"),
"--size-only",
"--fast-list",
"--filter",
"- deno/gen/file/**",
"--filter",
"+ deno/**",
"--filter",
"+ go/**",
"--filter",
"- *",
],
)
.await
{
tracing::info!("Failed to copy cache to bucket. Error: {:?}", e);
return Err(e);
}
tracing::info!(
"Finished copying cache to bucket {bucket}, took: {:?}s",
start.elapsed().as_secs()
);
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_to_bucket_as_tar(bucket: &str) {
tracing::info!("Copying cache to bucket {bucket} as tar");
let start = Instant::now();
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"tar",
vec![
"-c",
"-f",
&format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}"),
"go",
"deno",
],
)
.await
{
tracing::info!("Failed to tar cache. Error: {:?}", e);
return;
}
let tar_metadata =
tokio::fs::metadata(format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}")).await;
if tar_metadata.is_err() || tar_metadata.as_ref().unwrap().len() == 0 {
tracing::info!("Failed to tar cache");
return;
}
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"rclone",
vec![
"copyto",
&format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}"),
&format!(":s3,env_auth=true:{bucket}/{TAR_CACHE_FILENAME}"),
"-v",
"--size-only",
"--fast-list",
"--s3-no-check-bucket",
],
)
.await
{
tracing::info!("Failed to copy tar to bucket. Error: {:?}", e);
return;
}
if let Err(e) =
tokio::fs::remove_file(format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}")).await
{
tracing::info!("Failed to remove tar cache. Error: {:?}", e);
};
tracing::info!(
"Finished copying cache to bucket {bucket} as tar, took: {:?}s. Size of new tar: {}",
start.elapsed().as_secs(),
tar_metadata.unwrap().len()
);
}
#[cfg(feature = "enterprise")]
pub async fn copy_denogo_cache_from_bucket_as_tar(bucket: &str) {
use tokio::fs::metadata;
tracing::info!("Copying denogo cache from bucket {bucket} as tar");
let start: Instant = Instant::now();
if let Err(e) = execute_command(
ROOT_TMP_CACHE_DIR,
"rclone",
vec![
"copyto",
&format!(":s3,env_auth=true:{bucket}/{TAR_CACHE_FILENAME}"),
&format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}"),
"-v",
"--size-only",
"--fast-list",
],
)
.await
{
tracing::info!("Failed copying denogo tar from cache. Error: {:?}", e);
return;
}
if let Err(e) = execute_command(
ROOT_CACHE_DIR,
"tar",
vec![
"-xpvf",
&format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}"),
],
)
.await
{
tracing::info!("Failed to untar denogo. Error: {:?}", e);
return;
}
let denogen = format!("{ROOT_TMP_CACHE_DIR}deno/gen/file");
if metadata(&denogen).await.is_ok() {
let _ = tokio::fs::remove_dir_all(denogen).await;
}
if let Err(e) =
tokio::fs::remove_file(format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}")).await
{
tracing::info!("Failed to remove denotar cache. Error: {:?}", e);
return;
};
tracing::info!(
"Finished copying denogotar from bucket {bucket} as tar, took: {:?}s",
start.elapsed().as_secs()
);
}
#[cfg(feature = "enterprise")]
pub async fn copy_all_piptars_from_bucket(bucket: &str) {
tracing::info!("Copying all piptars cache from bucket {bucket}");
let start = Instant::now();
if let Err(e) = execute_command(
ROOT_CACHE_DIR,
"rclone",
vec![
"copy",
&format!(":s3,env_auth=true:{bucket}/tar/pip/"),
&TAR_PIP_TMP_CACHE_DIR,
"-v",
"--size-only",
"--fast-list",
],
)
.await
{
tracing::info!("Failed transferring all piptars from cache. Error: {:?}", e);
return;
}
tracing::info!(
"Finished transferring piptars from bucket {bucket} as tar, took: {:?}s",
start.elapsed().as_secs()
);
}
// async fn check_if_bucket_syncable(bucket: &str) -> bool {
// match Command::new("rclone")
// .arg("lsf")
// .arg(format!(":s3,env_auth=true:{bucket}/NOSYNC"))
// .arg("-vv")
// .arg("--fast-list")
// .stdin(Stdio::null())
// .stdout(Stdio::null())
// .output()
// .await;
// return true;
// }
#[cfg(feature = "enterprise")]
pub async fn copy_tmp_cache_to_cache() -> error::Result<()> {
let start: Instant = Instant::now();
execute_command(
TMP_DIR,
"rclone",
vec![
"sync",
ROOT_TMP_CACHE_DIR,
ROOT_CACHE_DIR,
"--filter",
"- deno/gen/file/**",
"--filter",
"+ deno/**",
"--filter",
"+ go/**",
"--filter",
"- *",
],
)
.await?;
tracing::info!(
"Finished copying local tmp cache to local cache. Took {}ms",
start.elapsed().as_millis(),
);
if let Err(e) = untar_all_piptars().await {
tracing::info!("Failed to untar piptars. Error: {:?}", e);
}
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn untar_all_piptars() -> error::Result<()> {
use tokio::fs::{self, metadata};
use crate::PIP_CACHE_DIR;
let start: Instant = Instant::now();
let mut entries = fs::read_dir(TAR_PIP_TMP_CACHE_DIR).await?;
while let Some(entry) = entries.next_entry().await? {
if let Err(e) = {
let entry_path = entry.path();
let path = entry_path.to_str().expect("Could not convert path to str");
let folder = format!(
"{PIP_CACHE_DIR}/{}",
path.split('/')
.last()
.unwrap()
.strip_suffix(".tar")
.unwrap()
);
if metadata(&folder).await.is_ok() {
continue;
}
extract_pip_tar(&path, &folder).await?;
Ok(()) as error::Result<()>
} {
tracing::info!("Failed to extract pip tar. Error: {:?}", e);
}
}
tracing::info!(
"Finished untarring all piptars. Took {}ms",
start.elapsed().as_millis(),
);
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn extract_pip_tar(tar: &str, folder: &str) -> error::Result<()> {
use tokio::fs;
let start: Instant = Instant::now();
fs::create_dir_all(&folder).await?;
if let Err(e) = execute_command(&folder, "tar", vec!["-xpvf", tar]).await {
tracing::info!("Failed to untar piptar. Error: {:?}", e);
fs::remove_dir_all(&folder).await?;
return Err(e);
}
tracing::info!(
"Finished extracting pip tar {folder}. Took {}ms",
start.elapsed().as_millis(),
);
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_to_tmp_cache() -> error::Result<()> {
let start: Instant = Instant::now();
execute_command(
TMP_DIR,
"rclone",
vec![
"sync",
ROOT_CACHE_DIR,
ROOT_TMP_CACHE_DIR,
"--filter",
"- deno/gen/file/**",
"--filter",
"+ deno/**",
"--filter",
"+ go/**",
"--filter",
"- *",
],
)
.await?;
tracing::info!(
"Finished copying local cache to local tmp cache. Took {}ms",
start.elapsed().as_millis()
);
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn execute_command(dir: &str, command: &str, args: Vec<&str>) -> error::Result<()> {
match Command::new(command)
.current_dir(dir)
.args(args.clone())
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
if !h.wait().await.unwrap().success() {
return Err(error::Error::ExecutionErr(format!(
"Failed to apply {command} with args: {}",
args.iter().join(" ")
)));
}
}
Err(e) => {
return Err(error::Error::ExecutionErr(format!(
"Failed to apply {command} with args: {}. Error: {e:?}",
args.iter().join(" ")
)));
}
}
Ok(())
}
+373
View File
@@ -0,0 +1,373 @@
use std::process::Stdio;
use itertools::Itertools;
use tokio::{
fs::{DirBuilder, File},
io::AsyncReadExt,
process::Command,
};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
utils::calculate_hash,
};
use windmill_parser_go::parse_go_imports;
use crate::{
common::{capitalize, read_result, set_logs},
create_args_and_out_file, get_reserved_variables, handle_child, write_file,
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, GOPRIVATE, GO_CACHE_DIR, HOME_ENV,
NETRC, NSJAIL_PATH, PATH_ENV,
};
const GO_REQ_SPLITTER: &str = "//go.sum\n";
const NSJAIL_CONFIG_RUN_GO_CONTENT: &str = include_str!("../nsjail/run.go.config.proto");
lazy_static::lazy_static! {
static ref GO_PATH: String = std::env::var("GO_PATH").unwrap_or_else(|_| "/usr/bin/go".to_string());
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_go_job(
logs: &mut String,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
inner_content: &str,
job_dir: &str,
requirements_o: Option<String>,
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
) -> Result<serde_json::Value, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
let (skip_go_mod, skip_tidy) = if let Some(requirements) = requirements_o {
gen_go_mod(inner_content, job_dir, &requirements).await?
} else {
(false, false)
};
logs.push_str("\n\n--- GO DEPENDENCIES SETUP ---\n");
set_logs(logs, &job.id, db).await;
install_go_dependencies(
&job.id,
inner_content,
logs,
job_dir,
db,
true,
skip_go_mod,
skip_tidy,
worker_name,
&job.workspace_id,
)
.await?;
logs.push_str("\n\n--- GO CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
let client = &client.get_authed().await;
create_args_and_out_file(client, job, job_dir).await?;
{
let sig = windmill_parser_go::parse_go_sig(&inner_content)?;
drop(inner_content);
const WRAPPER_CONTENT: &str = r#"package main
import (
"encoding/json"
"os"
"fmt"
"mymod/inner"
)
func main() {{
dat, err := os.ReadFile("args.json")
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
var req inner.Req
if err := json.Unmarshal(dat, &req); err != nil {{
fmt.Println(err)
os.Exit(1)
}}
res, err := inner.Run(req)
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
res_json, err := json.Marshal(res)
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
f, err := os.OpenFile("result.json", os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
_, err = f.WriteString(string(res_json))
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
}}"#;
write_file(job_dir, "main.go", WRAPPER_CONTENT).await?;
{
let spread = &sig
.args
.clone()
.into_iter()
.map(|x| format!("req.{}", capitalize(&x.name)))
.join(", ");
let req_body = &sig
.args
.into_iter()
.map(|x| {
format!(
"{} {} `json:\"{}\"`",
capitalize(&x.name),
windmill_parser_go::otyp_to_string(x.otyp),
x.name
)
})
.join("\n");
let runner_content: String = format!(
r#"package inner
type Req struct {{
{req_body}
}}
func Run(req Req) (interface{{}}, error){{
return main({spread})
}}
"#,
);
write_file(&format!("{job_dir}/inner"), "runner.go", &runner_content).await?;
}
}
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let child = if !*DISABLE_NSJAIL {
let _ = write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_GO_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", GO_CACHE_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount),
)
.await?;
let build_go = Command::new(GO_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("GOPATH", GO_CACHE_DIR)
.env("HOME", HOME_ENV.as_str())
.args(vec!["build", "main.go"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(
&job.id,
db,
logs,
build_go,
false,
worker_name,
&job.workspace_id,
"go build",
)
.await?;
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec!["--config", "run.config.proto", "--", "/tmp/go/main"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
} else {
if let Some(ref netrc) = *NETRC {
write_file(&HOME_ENV, ".netrc", netrc).await?;
}
Command::new(GO_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("GOPATH", GO_CACHE_DIR)
.env("GOPRIVATE", GOPRIVATE.as_ref().unwrap_or(&String::new()))
.env("HOME", HOME_ENV.as_str())
.args(vec!["run", "main.go"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
handle_child(
&job.id,
db,
logs,
child,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
"go run",
)
.await?;
read_result(job_dir).await
}
async fn gen_go_mod(
inner_content: &str,
job_dir: &str,
requirements: &str,
) -> error::Result<(bool, bool)> {
gen_go_mymod(inner_content, job_dir).await?;
let md = requirements.split_once(GO_REQ_SPLITTER);
if let Some((req, sum)) = md {
write_file(job_dir, "go.mod", &req).await?;
write_file(job_dir, "go.sum", &sum).await?;
Ok((true, true))
} else {
write_file(job_dir, "go.mod", &requirements).await?;
Ok((true, false))
}
}
pub async fn install_go_dependencies(
job_id: &Uuid,
code: &str,
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
non_dep_job: bool,
skip_go_mod: bool,
has_sum: bool,
worker_name: &str,
w_id: &str,
) -> error::Result<String> {
if !skip_go_mod {
gen_go_mymod(code, job_dir).await?;
let child = Command::new("go")
.current_dir(job_dir)
.args(vec!["mod", "init", "mymod"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name, w_id, "go init").await?;
}
let mut new_lockfile = false;
let hash = if !has_sum {
calculate_hash(parse_go_imports(&code)?.iter().join("\n").as_str())
} else {
"".to_string()
};
let mut skip_tidy = has_sum;
if !has_sum {
if let Some(cached) = sqlx::query_scalar!(
"SELECT lockfile FROM pip_resolution_cache WHERE hash = $1",
hash
)
.fetch_optional(db)
.await?
{
logs.push_str(&format!("\nfound cached resolution"));
gen_go_mod(code, job_dir, &cached).await?;
skip_tidy = true;
new_lockfile = false;
} else {
new_lockfile = true;
}
}
let mod_command = if skip_tidy { "download" } else { "tidy" };
let child = Command::new(GO_PATH.as_str())
.current_dir(job_dir)
.env("GOPATH", GO_CACHE_DIR)
.args(vec!["mod", mod_command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(
job_id,
db,
logs,
child,
false,
worker_name,
&w_id,
&format!("go {mod_command}"),
)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
if (!new_lockfile || has_sum) && non_dep_job {
return Ok("".to_string());
}
let mut req_content = "".to_string();
let mut file = File::open(format!("{job_dir}/go.mod")).await?;
file.read_to_string(&mut req_content).await?;
req_content.push_str(GO_REQ_SPLITTER);
let sum_path = format!("{job_dir}/go.sum");
if tokio::fs::metadata(&sum_path).await.is_ok() {
let mut file = File::open(sum_path).await?;
file.read_to_string(&mut req_content).await?;
}
if non_dep_job {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
hash,
req_content
).fetch_optional(db).await?;
return Ok(String::new());
} else {
Ok(req_content)
}
}
async fn gen_go_mymod(code: &str, job_dir: &str) -> error::Result<()> {
let code = if code.trim_start().starts_with("package") {
code.to_string()
} else {
format!("package inner; {code}")
};
let mymod_dir = format!("{job_dir}/inner");
DirBuilder::new()
.recursive(true)
.create(&mymod_dir)
.await
.expect("could not create go's mymod dir");
write_file(&mymod_dir, "inner_main.go", &code).await?;
Ok(())
}
+38 -23
View File
@@ -6,23 +6,32 @@
* 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, CLOUD_HOSTED};
use windmill_common::{
error::Error,
flow_status::FlowStatusModule,
jobs::{JobKind, QueuedJob},
schedule::Schedule,
METRICS_ENABLED,
};
use windmill_queue::{delete_job, schedule::get_schedule_opt, QueueTransaction, 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 +54,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 +93,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
@@ -116,9 +126,10 @@ pub async fn add_completed_job(
, email
, visible_to_owner
, mem_peak
, tag
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,\
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29)
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29, $30)
ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)",
queued_job.workspace_id,
queued_job.id,
@@ -148,12 +159,13 @@ pub async fn add_completed_job(
duration: Option<i64>,
queued_job.email,
queued_job.visible_to_owner,
mem_peak
mem_peak,
queued_job.tag,
)
.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
@@ -208,21 +220,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 +247,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,
+1 -1
View File
@@ -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.into())?;
let global = context.resolve_value(global).await?;
let scope = &mut context.handle_scope();
+4
View File
@@ -1,5 +1,9 @@
mod common;
mod global_cache;
mod go_executor;
mod jobs;
mod js_eval;
mod python_executor;
mod worker;
mod worker_flow;
-92
View File
@@ -1,92 +0,0 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
// use std::{net::SocketAddr, time::Duration};
// use anyhow::Context;
// use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
// use windmill_common::{
// error::{self, Error},
// utils::rd_string,
// };
// #[tokio::main]
// async fn main() -> anyhow::Result<()> {
// // dotenv().ok();
// windmill_common::tracing_init::initialize_tracing();
// let db = async {
// let database_url = std::env::var("DATABASE_URL")
// .map_err(|_| Error::BadConfig("DATABASE_URL env var is missing".to_string()))?;
// let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
// Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
// Err(_) => 10,
// };
// Ok::<Pool<Postgres>, error::Error>(
// PgPoolOptions::new()
// .max_connections(max_connections)
// .max_lifetime(Duration::from_secs(30 * 60)) // 30 mins
// .connect(&database_url)
// .await
// .map_err(|err| Error::ConnectingToDatabase(err.to_string()))?,
// )
// }
// .await?;
// let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
// .ok()
// .map(|s| {
// s.parse::<bool>()
// .map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
// .or_else(|_| s.parse::<SocketAddr>().map(Some))
// })
// .transpose()?
// .flatten();
// let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
// let shutdown_signal = windmill_common::shutdown_signal(tx);
// let workers_f = async {
// let instance_name = rd_string(5);
// let ip = windmill_common::external_ip::get_ip()
// .await
// .unwrap_or_else(|e| {
// tracing::warn!(error = e.to_string(), "failed to get external IP");
// "unretrievable IP".to_string()
// });
// let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5));
// windmill_worker::run_worker(
// &db.clone(),
// &instance_name,
// worker_name,
// 1,
// 1,
// &ip,
// rx.resubscribe(),
// )
// .await;
// Ok(()) as anyhow::Result<()>
// };
// let metrics_f = async {
// match metrics_addr {
// Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
// .await
// .map_err(anyhow::Error::from),
// None => Ok(()),
// }
// };
// futures::try_join!(shutdown_signal, workers_f, metrics_f)?;
// Ok(())
// }
@@ -0,0 +1,584 @@
use std::process::Stdio;
use itertools::Itertools;
use regex::Regex;
use sqlx::{Pool, Postgres};
use tokio::{
fs::{metadata, DirBuilder, File},
io::AsyncReadExt,
process::Command,
};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
utils::calculate_hash,
};
lazy_static::lazy_static! {
static ref PYTHON_PATH: String =
std::env::var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
static ref PIP_INDEX_URL: Option<String> = std::env::var("PIP_INDEX_URL").ok();
static ref PIP_EXTRA_INDEX_URL: Option<String> = std::env::var("PIP_EXTRA_INDEX_URL").ok();
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
static ref PIP_LOCAL_DEPENDENCIES: Option<Vec<String>> = {
let pip_local_dependencies = std::env::var("PIP_LOCAL_DEPENDENCIES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
if pip_local_dependencies == Some(vec!["".to_string()]) {
None
} else {
pip_local_dependencies
}
};
static ref ADDITIONAL_PYTHON_PATHS: Option<Vec<String>> = std::env::var("ADDITIONAL_PYTHON_PATHS")
.ok()
.map(|x| x.split(':').map(|x| x.to_string()).collect());
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
}
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
const RELATIVE_PYTHON_LOADER: &str = include_str!("../loader.py");
#[cfg(feature = "enterprise")]
use crate::global_cache::{build_tar_and_push, pull_from_tar};
use crate::{
common::{read_result, set_logs},
create_args_and_out_file, get_reserved_variables, handle_child, write_file,
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV,
PIP_CACHE_DIR, S3_CACHE_BUCKET,
};
pub async fn create_dependencies_dir(job_dir: &str) {
DirBuilder::new()
.recursive(true)
.create(&format!("{job_dir}/dependencies"))
.await
.expect("could not create dependencies dir");
}
pub async fn pip_compile(
job_id: &Uuid,
requirements: &str,
logs: &mut String,
job_dir: &str,
db: &Pool<Postgres>,
worker_name: &str,
w_id: &str,
) -> error::Result<String> {
logs.push_str(&format!("\nresolving dependencies..."));
set_logs(logs, job_id, db).await;
logs.push_str(&format!("\ncontent of requirements:\n{}", requirements));
let req_hash = calculate_hash(&requirements);
if let Some(cached) = sqlx::query_scalar!(
"SELECT lockfile FROM pip_resolution_cache WHERE hash = $1",
req_hash
)
.fetch_optional(db)
.await?
{
logs.push_str(&format!("\nfound cached resolution"));
return Ok(cached);
}
let file = "requirements.in";
let requirements = if let Some(pip_local_dependencies) = PIP_LOCAL_DEPENDENCIES.as_ref() {
let deps = pip_local_dependencies.clone();
requirements
.lines()
.filter(|s| !deps.contains(&s.to_string()))
.join("\n")
} else {
requirements.to_string()
};
write_file(job_dir, file, &requirements).await?;
let mut args = vec!["-q", "--no-header", file, "--resolver=backtracking"];
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
args.extend(["--extra-index-url", url]);
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
args.extend(["--index-url", url]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
let child = Command::new("pip-compile")
.current_dir(job_dir)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(
job_id,
db,
logs,
child,
false,
worker_name,
&w_id,
"pip-compile",
)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
let path_lock = format!("{job_dir}/requirements.txt");
let mut file = File::open(path_lock).await?;
let mut req_content = "".to_string();
file.read_to_string(&mut req_content).await?;
let lockfile = req_content
.lines()
.filter(|x| !x.trim_start().starts_with('#'))
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("\n");
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
req_hash,
lockfile
).fetch_optional(db).await?;
Ok(lockfile)
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_python_job(
requirements_o: Option<String>,
job_dir: &str,
worker_dir: &str,
worker_name: &str,
job: &QueuedJob,
logs: &mut String,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
inner_content: &String,
shared_mount: &str,
base_internal_url: &str,
) -> windmill_common::error::Result<serde_json::Value> {
create_dependencies_dir(job_dir).await;
let mut additional_python_paths: Vec<String> =
ADDITIONAL_PYTHON_PATHS.to_owned().unwrap_or_else(|| vec![]);
let requirements = match requirements_o {
Some(r) => r,
None => {
let requirements = windmill_parser_py::parse_python_imports(&inner_content)?.join("\n");
if requirements.is_empty() {
"".to_string()
} else {
pip_compile(
&job.id,
&requirements,
logs,
job_dir,
db,
worker_name,
&job.workspace_id,
)
.await
.map_err(|e| {
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
})?
}
}
};
if requirements.len() > 0 {
additional_python_paths = handle_python_reqs(
requirements
.split("\n")
.filter(|x| !x.starts_with("--"))
.collect(),
&job.id,
&job.workspace_id,
logs,
db,
worker_name,
job_dir,
worker_dir,
)
.await?;
}
logs.push_str("\n\n--- PYTHON CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
let relative_imports = RELATIVE_IMPORT_REGEX.is_match(&inner_content);
let script_path_splitted = &job.script_path().split("/");
let dirs_full = script_path_splitted
.clone()
.take(script_path_splitted.clone().count() - 1)
.join("/")
.replace("-", "_");
let dirs = if dirs_full.len() > 0 {
dirs_full
} else {
"tmp".to_string()
};
let last = script_path_splitted
.clone()
.last()
.unwrap()
.replace("-", "_")
.replace(" ", "_")
.to_lowercase();
let module_dir = format!("{}/{}", job_dir, dirs);
tokio::fs::create_dir_all(format!("{module_dir}/")).await?;
let _ = write_file(&module_dir, &format!("{last}.py"), inner_content).await?;
if relative_imports {
let _ = write_file(&job_dir, "loader.py", RELATIVE_PYTHON_LOADER).await?;
}
let sig = windmill_parser_py::parse_python_signature(inner_content)?;
let transforms = sig
.args
.iter()
.map(|x| match x.typ {
windmill_parser::Typ::Bytes => {
let name = &x.name;
format!(
"if \"{name}\" in kwargs and kwargs[\"{name}\"] is not None:\n \
kwargs[\"{name}\"] = base64.b64decode(kwargs[\"{name}\"])\n",
)
}
windmill_parser::Typ::Datetime => {
let name = &x.name;
format!(
"if \"{name}\" in kwargs and kwargs[\"{name}\"] is not None:\n \
kwargs[\"{name}\"] = datetime.strptime(kwargs[\"{name}\"], \
'%Y-%m-%dT%H:%M')\n",
)
}
_ => "".to_string(),
})
.collect::<Vec<String>>()
.join("");
let client = client.get_authed().await;
create_args_and_out_file(&client, job, job_dir).await?;
let import_loader = if relative_imports {
"import loader"
} else {
""
};
let import_base64 = if sig
.args
.iter()
.any(|x| x.typ == windmill_parser::Typ::Bytes)
{
"import base64"
} else {
""
};
let import_datetime = if sig
.args
.iter()
.any(|x| x.typ == windmill_parser::Typ::Datetime)
{
"from datetime import datetime"
} else {
""
};
let spread = if sig.star_kwargs {
"args = kwargs".to_string()
} else {
sig.args
.into_iter()
.map(|x| {
let name = &x.name;
if x.default.is_none() {
format!("args[\"{name}\"] = kwargs.get(\"{name}\")")
} else {
format!(
r#"args["{name}"] = kwargs.get("{name}")
if args["{name}"] is None:
del args["{name}"]"#
)
}
})
.join("\n")
};
let module_dir_dot = dirs.replace("/", ".").replace("-", "_");
let wrapper_content: String = format!(
r#"
import json
{import_loader}
{import_base64}
{import_datetime}
import traceback
import sys
from {module_dir_dot} import {last} as inner_script
with open("args.json") as f:
kwargs = json.load(f, strict=False)
args = {{}}
{transforms}
{spread}
for k, v in list(args.items()):
if v == '<function call>':
del args[k]
try:
res = inner_script.main(**args)
typ = type(res)
if typ.__name__ == 'DataFrame':
if typ.__module__ == 'pandas.core.frame':
res = res.values.tolist()
elif typ.__module__ == 'polars.dataframe.frame':
res = res.rows()
res_json = json.dumps(res, separators=(',', ':'), default=str).replace('\n', '')
with open("result.json", 'w') as f:
f.write(res_json)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
tb = traceback.format_tb(exc_traceback)
with open("result.json", 'w') as f:
err_json = json.dumps({{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}, separators=(',', ':'), default=str).replace('\n', '')
f.write(err_json)
sys.exit(1)
"#,
);
write_file(job_dir, "wrapper.py", &wrapper_content).await?;
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let additional_python_paths_folders = additional_python_paths.iter().join(":");
if !*DISABLE_NSJAIL {
let shared_deps = additional_python_paths
.into_iter()
.map(|pp| {
format!(
r#"
mount {{
src: "{pp}"
dst: "{pp}"
is_bind: true
rw: false
}}
"#
)
})
.join("\n");
let _ = write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_PYTHON3_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
.replace("{MAIN}", format!("{dirs}/{last}").as_str())
.replace(
"{ADDITIONAL_PYTHON_PATHS}",
additional_python_paths_folders.as_str(),
),
)
.await?;
} else {
reserved_variables.insert("PYTHONPATH".to_string(), additional_python_paths_folders);
}
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
"started python code execution {}",
job.id
);
let child = if !*DISABLE_NSJAIL {
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
.env_clear()
// inject PYTHONPATH here - for some reason I had to do it in nsjail conf
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec![
"--config",
"run.config.proto",
"--",
PYTHON_PATH.as_str(),
"-u",
"-m",
"wrapper",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
} else {
Command::new(PYTHON_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec!["-u", "-m", "wrapper"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
handle_child(
&job.id,
db,
logs,
child,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
"python run",
)
.await?;
read_result(job_dir).await
}
pub async fn handle_python_reqs(
requirements: Vec<&str>,
job_id: &Uuid,
w_id: &str,
logs: &mut String,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
job_dir: &str,
worker_dir: &str,
) -> error::Result<Vec<String>> {
let mut req_paths: Vec<String> = vec![];
let mut vars = vec![("PATH", PATH_ENV.as_str())];
if !*DISABLE_NSJAIL {
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
vars.push(("EXTRA_INDEX_URL", url));
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
vars.push(("INDEX_URL", url));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
vars.push(("TRUSTED_HOST", host));
}
let _ = write_file(
job_dir,
"download.config.proto",
&NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
.replace("{WORKER_DIR}", &worker_dir)
.replace("{CACHE_DIR}", PIP_CACHE_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)
.await?;
};
for req in requirements {
// todo: handle many reqs
let venv_p = format!("{PIP_CACHE_DIR}/{req}");
if metadata(&venv_p).await.is_ok() {
req_paths.push(venv_p);
continue;
}
#[cfg(feature = "enterprise")]
if let Some(ref bucket) = *S3_CACHE_BUCKET {
sqlx::query_scalar!("UPDATE queue SET last_ping = now() WHERE id = $1", job_id)
.execute(db)
.await?;
if pull_from_tar(bucket, venv_p.clone()).await.is_ok() {
req_paths.push(venv_p.clone());
continue;
}
}
logs.push_str("\n--- PIP INSTALL ---\n");
logs.push_str(&format!("\n{req} is being installed for the first time.\n It will be cached for all ulterior uses."));
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"started setup python dependencies"
);
let child = if !*DISABLE_NSJAIL {
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"starting nsjail"
);
let mut vars = vars.clone();
let req = req.to_string();
vars.push(("REQ", &req));
vars.push(("TARGET", &venv_p));
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(vars)
.args(vec!["--config", "download.config.proto"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
} else {
let mut args = vec![
"-m",
"pip",
"install",
&req,
"-I",
"--no-deps",
"--no-color",
"--isolated",
"--no-warn-conflicts",
"--disable-pip-version-check",
"-t",
venv_p.as_str(),
];
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
args.extend(["--extra-index-url", url]);
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
args.extend(["--index-url", url]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", &host]);
}
Command::new(PYTHON_PATH.as_str())
.env_clear()
.env("PATH", PATH_ENV.as_str())
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
let child = handle_child(
&job_id,
db,
logs,
child,
false,
worker_name,
&w_id,
&format!("pip install {req}"),
)
.await;
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
is_ok = child.is_ok(),
"finished setting up python dependencies {}",
job_id
);
child?;
#[cfg(feature = "enterprise")]
if let Some(ref bucket) = *S3_CACHE_BUCKET {
let venv_p = venv_p.clone();
tokio::spawn(build_tar_and_push(bucket, venv_p));
}
req_paths.push(venv_p);
}
Ok(req_paths)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-53
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
View File
@@ -0,0 +1,7 @@
{
"type": "PREVIEW_SCRIPT",
"workspace": "demo",
"language": "deno",
"args": {},
"content": "export async function main() { return \"Hello World\"; }"
}
+39 -37
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();
+19 -14
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++;
}
+3
View File
@@ -48,6 +48,9 @@ wmill sync pull
wmill sync push
```
We recommend using the --yaml option to use yaml instead of json as the encoding
format. Yaml will be made the default soon.
### Pushing individual files
You can push individual resources using
+48 -109
View File
@@ -1,120 +1,55 @@
// deno-lint-ignore-file no-explicit-any
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,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
isSuperset,
parseFromFile,
removeType,
} from "./types.ts";
@model()
export class AppFile implements Resource, PushDiffs {
@property(Any)
export interface AppFile {
value: any;
@property(() => String)
summary: string;
@property(Any)
policy: Policy;
}
constructor(value: string, summary: string, policy: Policy) {
this.value = value;
this.summary = summary;
this.policy = policy;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[]
): Promise<void> {
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... ${remotePath}`
)
);
const changeset: {
summary?: string | undefined;
value?: any;
policy?: Policy | undefined;
} = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
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);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
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;
}
await AppService.updateApp({
workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.yellow.bold("Creating new app..."));
await AppService.createApp({
workspace,
requestBody: {
path: remotePath,
policy: this.policy,
summary: this.summary,
value: this.value,
},
});
export async function pushApp(
workspace: string,
remotePath: string,
app: AppFile | AppWithLastVersion | undefined,
newApp: AppFile
): Promise<void> {
remotePath = removeType(remotePath, "app");
if (app) {
if (isSuperset(newApp, app)) {
return;
}
}
async push(workspace: string, remotePath: string): Promise<void> {
await this.pushDiffs(
await AppService.updateApp({
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false })
);
path: remotePath,
requestBody: {
...newApp,
},
});
} else {
console.log(colors.yellow.bold("Creating new app..."));
await AppService.createApp({
workspace,
requestBody: {
path: remotePath,
...newApp,
},
});
}
}
@@ -154,19 +89,23 @@ async function push(opts: GlobalOptions, filePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await pushApp(filePath, workspace.workspaceId, remotePath);
console.log(colors.bold.underline.green("App pushed"));
}
let app: AppWithLastVersion | undefined = undefined;
try {
app = await AppService.getAppByPath({
workspace: workspace.workspaceId,
path: remotePath,
});
} catch {
// app doesn't exist
}
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);
await pushApp(
workspace.workspaceId,
remotePath,
app,
parseFromFile(filePath)
);
console.log(colors.bold.underline.green("App pushed"));
}
const command = new Command()
-8
View File
@@ -1,8 +0,0 @@
// globally shared decoverto instance
import { Decoverto } from "npm:decoverto";
const decoverto = new Decoverto();
// TODO: Properly type FlowModule
export { Any, array, map, MapShape, model, property } from "npm:decoverto";
export { decoverto };
+74 -107
View File
@@ -1,121 +1,94 @@
// deno-lint-ignore-file no-explicit-any
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import { GlobalOptions, isSuperset } from "./types.ts";
import { parse as yamlParse } from "https://deno.land/std@0.184.0/yaml/mod.ts";
import {
colors,
Command,
Flow,
FlowModule,
FlowService,
JobService,
microdiff,
OpenFlowWPath,
Table,
} from "./deps.ts";
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 {
@property(() => String)
export interface FlowFile {
summary: string;
@property(() => String)
description?: string;
@property(Any)
value: any;
@property(Any)
schema?: any;
}
constructor(value: any, summary?: string) {
this.summary = summary ?? "";
this.value = value;
const alreadySynced: string[] = [];
export async function pushFlow(
workspace: string,
remotePath: string,
localFlowPath: string,
workspaceId: string
): Promise<void> {
if (alreadySynced.includes(localFlowPath)) {
return;
}
alreadySynced.push(localFlowPath);
let flow: Flow | undefined = undefined;
try {
flow = await FlowService.getFlowByPath({
workspace: workspaceId,
path: remotePath,
});
} catch {
// flow doesn't exist
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[]
): Promise<void> {
if (
await FlowService.existsFlowByPath({
workspace: workspace,
path: remotePath,
})
) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`
)
);
// TODO: Make these optional in backend (not path ofc)
const changeset: OpenFlowWPath = {
path: remotePath,
summary: this.summary,
value: this.value,
description: this.description, // This is OpenAPIed as optional, but isn't
schema: this.schema, // Same
};
const base_changeset = { ...changeset };
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
))
) {
throw new Error("Invalid flow diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some(
(v) => v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
return;
if (!localFlowPath.endsWith("/")) {
localFlowPath += "/";
}
const localFlowRaw = await Deno.readTextFile(localFlowPath + "flow.yaml");
const localFlow = yamlParse(localFlowRaw) as FlowFile;
function replaceInlineScripts(modules: FlowModule[]) {
modules.forEach((m) => {
if (m.value.type == "rawscript") {
const path = m.value.content.split(" ")[1];
m.value.content = Deno.readTextFileSync(localFlowPath + path);
} else if (m.value.type == "forloopflow") {
replaceInlineScripts(m.value.modules);
} else if (m.value.type == "branchall") {
m.value.branches.forEach((b) => replaceInlineScripts(b.modules));
} else if (m.value.type == "branchone") {
m.value.branches.forEach((b) => replaceInlineScripts(b.modules));
replaceInlineScripts(m.value.default);
}
});
}
const update = {
...changeset,
...base_changeset,
};
replaceInlineScripts(localFlow.value.modules);
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
requestBody: update,
});
} else {
console.log(colors.bold.yellow("Creating new flow..."));
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath,
summary: this.summary,
value: this.value,
schema: this.schema,
description: this.description,
},
});
if (flow) {
if (isSuperset(localFlow, flow)) {
console.log(colors.bold.green("Flow is up to date"));
return;
}
}
async push(workspace: string, remotePath: string): Promise<void> {
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false })
);
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
requestBody: {
path: remotePath,
...localFlow,
},
});
} else {
console.log(colors.bold.yellow("Creating new flow..."));
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath,
...localFlow,
},
});
}
}
@@ -128,21 +101,15 @@ async function push(opts: Options, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
await pushFlow(filePath, workspace.workspaceId, remotePath);
await pushFlow(
workspace.workspaceId,
remotePath,
filePath,
workspace.workspaceId
);
console.log(colors.bold.underline.green("Flow pushed"));
}
export async function pushFlow(
filePath: string,
workspace: string,
remotePath: string
) {
const data = decoverto
.type(FlowFile)
.rawToInstance(await Deno.readTextFile(filePath));
await data.push(workspace, remotePath);
}
async function list(opts: GlobalOptions & { showArchived?: boolean }) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);

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