Compare commits
14 Commits
v1.246.10
...
operator-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76266f3efb | ||
|
|
2cdda619de | ||
|
|
37744ff583 | ||
|
|
dbe14757d4 | ||
|
|
12bd4af3ee | ||
|
|
9cdaf9c201 | ||
|
|
ec094e8897 | ||
|
|
cf61e09b90 | ||
|
|
6a0ae83b49 | ||
|
|
2f4be74e94 | ||
|
|
28811023a4 | ||
|
|
e80534ca65 | ||
|
|
95be0155dc | ||
|
|
004781cd3d |
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@@ -101,7 +101,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
features=enterprise,enterprise_saml,stripe
|
||||
features=enterprise,enterprise_saml
|
||||
nsjail=true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,4 +5,3 @@ local/
|
||||
frontend/src/routes/test.svelte
|
||||
CaddyfileRemoteMalo
|
||||
*.swp
|
||||
**/.idea/
|
||||
|
||||
242
CHANGELOG.md
242
CHANGELOG.md
@@ -1,248 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## [1.246.10](https://github.com/windmill-labs/windmill/compare/v1.246.9...v1.246.10) (2024-01-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make cli backcompatible with respect to lockfile ([eb000f1](https://github.com/windmill-labs/windmill/commit/eb000f1fbc697869aab46b3be430c8d56b7a7e8c))
|
||||
* make some eval onDemandOnly ([36905da](https://github.com/windmill-labs/windmill/commit/36905daef60d78725d5b78d70e314281ed297565))
|
||||
|
||||
## [1.246.9](https://github.com/windmill-labs/windmill/compare/v1.246.8...v1.246.9) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid too long diffs in cli ([828cdd4](https://github.com/windmill-labs/windmill/commit/828cdd45f08a12071a028052b19503a441f047b1))
|
||||
|
||||
## [1.246.8](https://github.com/windmill-labs/windmill/compare/v1.246.7...v1.246.8) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve lockfile handling for cli ([d00de26](https://github.com/windmill-labs/windmill/commit/d00de2640abfe6265f12ee4f237c9ee5ba3e00db))
|
||||
|
||||
## [1.246.7](https://github.com/windmill-labs/windmill/compare/v1.246.6...v1.246.7) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve lockfile handling for cli ([8a6ea49](https://github.com/windmill-labs/windmill/commit/8a6ea496022ad70c658121e9694b634177dcc578))
|
||||
|
||||
## [1.246.6](https://github.com/windmill-labs/windmill/compare/v1.246.5...v1.246.6) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve lockfile handling for cli ([0e9b649](https://github.com/windmill-labs/windmill/commit/0e9b649d03f79cdf6d5bece5dc0ae8072e22f5b0))
|
||||
|
||||
## [1.246.5](https://github.com/windmill-labs/windmill/compare/v1.246.4...v1.246.5) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve app push for cli ([e6de809](https://github.com/windmill-labs/windmill/commit/e6de809ff29a9df54af5fef3f425072e24de49ec))
|
||||
* lock file in metadata is now a string ([#3027](https://github.com/windmill-labs/windmill/issues/3027)) ([8752dcb](https://github.com/windmill-labs/windmill/commit/8752dcbb191279b44a2b86ee0ed45ab040465b96))
|
||||
|
||||
## [1.246.4](https://github.com/windmill-labs/windmill/compare/v1.246.3...v1.246.4) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** Fix deno logo + add missing onDestroy ([#3025](https://github.com/windmill-labs/windmill/issues/3025)) ([b9de44c](https://github.com/windmill-labs/windmill/commit/b9de44c2b7daf964834af1d3598dbf40971e4a4f))
|
||||
* make DisplayResult more resilient ([b42c84d](https://github.com/windmill-labs/windmill/commit/b42c84df40e234436e7fcf30a64317e654bed3ed))
|
||||
* wmill app push <path_to_app_file> ([#3024](https://github.com/windmill-labs/windmill/issues/3024)) ([803962a](https://github.com/windmill-labs/windmill/commit/803962a943001ff5f2a58bf36f955cc16e92d2b1))
|
||||
|
||||
## [1.246.3](https://github.com/windmill-labs/windmill/compare/v1.246.2...v1.246.3) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* oidc token generation endpoint GET -> POST ([1f3e374](https://github.com/windmill-labs/windmill/commit/1f3e374b85581da463fda8727d9379d6711b7da8))
|
||||
|
||||
## [1.246.2](https://github.com/windmill-labs/windmill/compare/v1.246.1...v1.246.2) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* oidc token generation endpoint GET -> POST ([3119830](https://github.com/windmill-labs/windmill/commit/3119830062e9d4e30438950e208a2dde4eb12759))
|
||||
|
||||
## [1.246.1](https://github.com/windmill-labs/windmill/compare/v1.246.0...v1.246.1) (2024-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* expose getIdToken in python-client ([4604ccd](https://github.com/windmill-labs/windmill/commit/4604ccde7dd656627605b8c55256cc3628235cab))
|
||||
* expose getIdToken in typescript-client ([6568c9f](https://github.com/windmill-labs/windmill/commit/6568c9f93aa477c142bde6d989e4eec3a2440687))
|
||||
|
||||
## [1.246.0](https://github.com/windmill-labs/windmill/compare/v1.245.1...v1.246.0) (2024-01-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* OIDC support ([#3017](https://github.com/windmill-labs/windmill/issues/3017)) ([640ebcb](https://github.com/windmill-labs/windmill/commit/640ebcb146eae371abfa637a4f55fe1919aab013))
|
||||
|
||||
## [1.245.1](https://github.com/windmill-labs/windmill/compare/v1.245.0...v1.245.1) (2024-01-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* CLI script generate-metadata creates a default file if none exist ([#3015](https://github.com/windmill-labs/windmill/issues/3015)) ([eb48e0a](https://github.com/windmill-labs/windmill/commit/eb48e0a1071d33e19425f2228e029453b3484458))
|
||||
|
||||
## [1.245.0](https://github.com/windmill-labs/windmill/compare/v1.244.4...v1.245.0) (2024-01-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add script bootstrap and script generate-metadata CLI commands ([#3007](https://github.com/windmill-labs/windmill/issues/3007)) ([b9bee40](https://github.com/windmill-labs/windmill/commit/b9bee403f1ee922c776cf7a82aef0cdfc04c4c10))
|
||||
* Browse s3 bucket content from workspace settings page ([#3013](https://github.com/windmill-labs/windmill/issues/3013)) ([1053979](https://github.com/windmill-labs/windmill/commit/10539790d20e01faf7aa992f44c89ab623a794c0))
|
||||
* **frontend:** Add running runs on the script detail page ([#3005](https://github.com/windmill-labs/windmill/issues/3005)) ([c93932a](https://github.com/windmill-labs/windmill/commit/c93932a5b3b17cf3ddc7c86bf047343dc5da114d))
|
||||
* **frontend:** add specific bun and deno icons ([#3006](https://github.com/windmill-labs/windmill/issues/3006)) ([e9ab3ce](https://github.com/windmill-labs/windmill/commit/e9ab3ceac901503c6d6e0af0954516af80e1f4a0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* s3 resource is accessed by backend with admin permissions ([#3011](https://github.com/windmill-labs/windmill/issues/3011)) ([9fffe4f](https://github.com/windmill-labs/windmill/commit/9fffe4f6f578665242612a596300e93b6cf2e6b6))
|
||||
|
||||
## [1.244.4](https://github.com/windmill-labs/windmill/compare/v1.244.2...v1.244.4) (2024-01-15)
|
||||
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* experimental nodejs support ([047ee10](https://github.com/windmill-labs/windmill/commit/047ee10246f8e1bb952d3b8cdf21612948ac9843))
|
||||
* git sync branch name no contains the workspace ID and the type ([#3004](https://github.com/windmill-labs/windmill/issues/3004))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* git sync branch name no contains the workspace ID and the type ([#3004](https://github.com/windmill-labs/windmill/issues/3004)) ([d845864](https://github.com/windmill-labs/windmill/commit/d845864872aff0057d6a3f5d9df2cc4a8c642be5))
|
||||
* improve bun imports resolutions ([2b28854](https://github.com/windmill-labs/windmill/commit/2b288542bdfdba62b9182002db6c8c23cdd9869f))
|
||||
* improve bun imports resolutions ([64e592b](https://github.com/windmill-labs/windmill/commit/64e592b874ef0414dcea2a3dd113d323cff0661e))
|
||||
|
||||
## [1.244.2](https://github.com/windmill-labs/windmill/compare/v1.244.1...v1.244.2) (2024-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve favorite menu ([a1f93a4](https://github.com/windmill-labs/windmill/commit/a1f93a495e36e04b40c58bb8e33ab2336628ba8b))
|
||||
|
||||
## [1.244.1](https://github.com/windmill-labs/windmill/compare/v1.244.0...v1.244.1) (2024-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* menu colors ([b84cd6d](https://github.com/windmill-labs/windmill/commit/b84cd6d52d723fea55a407d347afec1c669da03a))
|
||||
* menu colors ([5201dcd](https://github.com/windmill-labs/windmill/commit/5201dcdd679690bf82b2606d85829ae9333e09ac))
|
||||
|
||||
## [1.244.0](https://github.com/windmill-labs/windmill/compare/v1.243.0...v1.244.0) (2024-01-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** Operator mode ([#2973](https://github.com/windmill-labs/windmill/issues/2973)) ([aaff17f](https://github.com/windmill-labs/windmill/commit/aaff17f813ae3f31cae58bb40b9c8118e772a2d8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add ability to set secret variable from python ([0733dd1](https://github.com/windmill-labs/windmill/commit/0733dd118e463f73caa9155b97fc8d9b02759e06))
|
||||
* improve oauth accounts permissions ([1621975](https://github.com/windmill-labs/windmill/commit/16219755a7fa8b9ff5a901c18842a1eceb68086a))
|
||||
* improve table behavior when searching ([f0c4901](https://github.com/windmill-labs/windmill/commit/f0c4901c218cfc4564f9f2be0cacaf3d9997d822))
|
||||
|
||||
## [1.243.0](https://github.com/windmill-labs/windmill/compare/v1.242.0...v1.243.0) (2024-01-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for bun install scopes ([d785def](https://github.com/windmill-labs/windmill/commit/d785deff3312b076e3ed9043924f514f8823e041))
|
||||
* **frontend:** s3 file upload ([#2976](https://github.com/windmill-labs/windmill/issues/2976)) ([3c59fb8](https://github.com/windmill-labs/windmill/commit/3c59fb8b4d8a80077c2f352ccf1314fd32ff442c))
|
||||
* GIt sync to multiple repo ([#2996](https://github.com/windmill-labs/windmill/issues/2996)) ([fda0e28](https://github.com/windmill-labs/windmill/commit/fda0e28db31fba9f71be6db7280696c3949fd7be))
|
||||
* support dynamic args in tags ([aafd7d9](https://github.com/windmill-labs/windmill/commit/aafd7d90037bae20808e836d34c352ca6b357155))
|
||||
* Tag override for scheduled scripts ([#2998](https://github.com/windmill-labs/windmill/issues/2998)) ([99484bd](https://github.com/windmill-labs/windmill/commit/99484bdc20bac966ca1d9d45ca4278133ea91b87))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow for any extra bunfig config ([e200889](https://github.com/windmill-labs/windmill/commit/e200889cff6c5166ce6564d7f9d606c1613fe03b))
|
||||
|
||||
## [1.242.0](https://github.com/windmill-labs/windmill/compare/v1.241.0...v1.242.0) (2024-01-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Instance group management page ([#2994](https://github.com/windmill-labs/windmill/issues/2994)) ([5f54f55](https://github.com/windmill-labs/windmill/commit/5f54f557903792ea6307b17f966c216cbd402709))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* catch more agGrid errors ([01dbf54](https://github.com/windmill-labs/windmill/commit/01dbf548f712c2e669038e4eb704c470d2cb0426))
|
||||
|
||||
## [1.241.0](https://github.com/windmill-labs/windmill/compare/v1.240.0...v1.241.0) (2024-01-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Instance group CRUD API ([#2992](https://github.com/windmill-labs/windmill/issues/2992)) ([5a157c4](https://github.com/windmill-labs/windmill/commit/5a157c415e7f075d3f57872a9adf7f5130632bb6))
|
||||
* make dedicated workers for flows able to share runtime for the same scripts ([d59b89e](https://github.com/windmill-labs/windmill/commit/d59b89ec3cdf2285c0eac56d7678fc0b9a2c2a32))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add cache for flow as flow step ([28ac763](https://github.com/windmill-labs/windmill/commit/28ac7632cf767b1c0df30d93c841ad32bf891202))
|
||||
* git sync now works for delete and rename ([#2988](https://github.com/windmill-labs/windmill/issues/2988)) ([cde574b](https://github.com/windmill-labs/windmill/commit/cde574b8910bbaf737e0d7e515c42fc796911c5e))
|
||||
|
||||
## [1.240.0](https://github.com/windmill-labs/windmill/compare/v1.239.0...v1.240.0) (2024-01-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** introduce --stateful for CLI, in preparation for --raw to become the default ([39ecf80](https://github.com/windmill-labs/windmill/commit/39ecf8017ee88c4311cb2dd580b20cd59066612e))
|
||||
|
||||
## [1.239.0](https://github.com/windmill-labs/windmill/compare/v1.238.0...v1.239.0) (2024-01-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add configurable ordering for script's generated UI ([717ccc9](https://github.com/windmill-labs/windmill/commit/717ccc94a06ee65a5676c7d9091faf892396657e))
|
||||
* **frontend:** AG chart ([#2972](https://github.com/windmill-labs/windmill/issues/2972)) ([e3c1661](https://github.com/windmill-labs/windmill/commit/e3c166154da4251e21a5f6a6fcf5b2e101b558e1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** improve .wmillignore handling of folders ([6996c90](https://github.com/windmill-labs/windmill/commit/6996c9083d9ae93922caf68e5658f18b49678630))
|
||||
* handle Etc/Unknown timezone better ([8c97ef0](https://github.com/windmill-labs/windmill/commit/8c97ef0394925b49a04c253259783be301e8f4ee))
|
||||
* only cache flow if it's a success ([589e683](https://github.com/windmill-labs/windmill/commit/589e683c27f15fcb48993f2f6e91523abef1794a))
|
||||
* spelling error dtails -> details ([#2986](https://github.com/windmill-labs/windmill/issues/2986)) ([308c4ce](https://github.com/windmill-labs/windmill/commit/308c4ceb475fef033ea87af5f772e1f35b4fb16d))
|
||||
|
||||
## [1.238.0](https://github.com/windmill-labs/windmill/compare/v1.237.0...v1.238.0) (2024-01-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to use secrets in pip requirements ([3517c7f](https://github.com/windmill-labs/windmill/commit/3517c7f28389a4ddf46a6bef4f2044aa94174050))
|
||||
* add support for multiselect in python ([c8a793d](https://github.com/windmill-labs/windmill/commit/c8a793d35ea843fb75428182edc06cfe7105af3b))
|
||||
* **frontend:** add shortcuts to audit logs ([#2975](https://github.com/windmill-labs/windmill/issues/2975)) ([4147d16](https://github.com/windmill-labs/windmill/commit/4147d1604a4c50ad0c667b413d64b44b357fb7e4))
|
||||
* **frontend:** Display the index by which a node can be selected with the setTab function + add tooltip ([#2974](https://github.com/windmill-labs/windmill/issues/2974)) ([cffae36](https://github.com/windmill-labs/windmill/commit/cffae3633107d9359e04354ba01876ccaf98193a))
|
||||
* S3 multipart upload accepts a custom S3 resource ([#2982](https://github.com/windmill-labs/windmill/issues/2982)) ([eea0d92](https://github.com/windmill-labs/windmill/commit/eea0d92dd11a3607fbb156e2bf3a3a001ab5e6a0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* main compile ([#2983](https://github.com/windmill-labs/windmill/issues/2983)) ([6d5c3f0](https://github.com/windmill-labs/windmill/commit/6d5c3f0f2252be15bf0169a42f5baa06d592911c))
|
||||
|
||||
## [1.237.0](https://github.com/windmill-labs/windmill/compare/v1.236.2...v1.237.0) (2024-01-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* make okta SSO configurable using custom domain ([4f01ee8](https://github.com/windmill-labs/windmill/commit/4f01ee89de743eab6ae323dea1ec4190ee23e0bf))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* delete is captured in more fields in app ([d340fbc](https://github.com/windmill-labs/windmill/commit/d340fbc02fd9500249c1d23799c1bdbfe9602cb8))
|
||||
|
||||
## [1.236.2](https://github.com/windmill-labs/windmill/compare/v1.236.1...v1.236.2) (2024-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -193,12 +193,6 @@ RUN set -eux; \
|
||||
ENV PATH="${PATH}:/usr/local/go/bin"
|
||||
ENV GO_PATH=/usr/local/go/bin/go
|
||||
|
||||
ARG nsjail=""
|
||||
|
||||
RUN if [ "$nsjail" = "true" ]; then apt-get -y update \
|
||||
&& apt-get install -y \
|
||||
curl nodejs; fi
|
||||
|
||||
# go build is slower the first time it is ran, so we prewarm it in the build
|
||||
RUN mkdir -p /tmp/gobuildwarm && cd /tmp/gobuildwarm && go mod init gobuildwarm && printf "package foo\nimport (\"fmt\")\nfunc main() { fmt.Println(42) }" > warm.go && go build -x && rm -rf /tmp/gobuildwarm
|
||||
|
||||
@@ -215,7 +209,7 @@ RUN chmod 755 /usr/bin/deno
|
||||
|
||||
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
COPY --from=oven/bun:canary /usr/local/bin/bun /usr/bin/bun
|
||||
COPY --from=oven/bun:1.0.18 /usr/local/bin/bun /usr/bin/bun
|
||||
|
||||
# add the docker client to call docker from a worker if enabled
|
||||
COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
|
||||
|
||||
37
README.md
37
README.md
@@ -357,6 +357,8 @@ you to have it being synced automatically everyday.
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
|
||||
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
|
||||
| DENO_EXTRA_IMPORT_MAP | None | extra import map to use to run deno scripts (format: `key=value,key=value`) | 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 |
|
||||
@@ -393,6 +395,41 @@ you to have it being synced automatically everyday.
|
||||
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
|
||||
| DISABLE_SERVER | false | Disable the external API, operate as a worker only instance | Worker |
|
||||
|
||||
## Run a local dev setup
|
||||
|
||||
### only Frontend
|
||||
|
||||
This will use the backend of <https://app.windmill.dev> but your own frontend
|
||||
with hot-code reloading.
|
||||
|
||||
1. Go to `frontend/`:
|
||||
1. `npm install`
|
||||
2. `npm run generate-backend-client`
|
||||
3. `npm run dev`
|
||||
2. Et voilà, windmill should be available at `http://localhost:3000/`
|
||||
|
||||
### Backend + Frontend
|
||||
|
||||
See the [./frontend/README_DEV.md](./frontend/README_DEV.md) file for all
|
||||
running options.
|
||||
|
||||
1. Create a Postgres Database for Windmill and create an admin role inside your
|
||||
Postgres setup. The easiest way to get a working postgres is running
|
||||
`cargo install --version ^0.7 sqlx-cli && sqlx migrate run`. This will also
|
||||
avoid compile time issue with sqlx's `query!` macro
|
||||
2. Install [nsjail](https://github.com/google/nsjail) and have it accessible in
|
||||
your PATH
|
||||
3. Install deno and python3, have the bins at `/usr/bin/deno` and
|
||||
`/usr/local/bin/python3`
|
||||
4. Install [caddy](https://caddyserver.com)
|
||||
5. Go to `frontend/`:
|
||||
1. `npm install`, `npm run generate-backend-client` then `npm run dev`
|
||||
2. In another shell `npm run build` otherwise the backend will not find the
|
||||
`frontend/build` folder and will crash
|
||||
3. In another shell `sudo caddy run --config Caddyfile`
|
||||
6. Go to `backend/`:
|
||||
`DATABASE_URL=<DATABASE_URL_TO_YOUR_WINDMILL_DB> RUST_LOG=info cargo run`
|
||||
7. Et voilà, windmill should be available at `http://localhost/`
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, is_secret, path from variable WHERE variable.path = $1 AND variable.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_secret",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1587639f13a1a3ad97bd324df38661f3e846f1992704c86ddad74dea151b0ec4"
|
||||
}
|
||||
@@ -125,11 +125,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "jobs",
|
||||
"type_info": "JsonArray"
|
||||
}
|
||||
@@ -166,7 +161,6 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "54bb09f68de5615a75b2174032e5fcebc314eeeccbcc609ba154fd062fe0f2fd"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT job FROM token WHERE token = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "55cf43cb9219b43f8e9f94b23b62846cd0b1ef5f64d20b0d975d0058730f427b"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry, summary, no_flow_overlap, tag ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22 ) RETURNING *",
|
||||
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry, summary, no_flow_overlap ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 ) RETURNING *",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -151,8 +146,7 @@
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar"
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -179,9 +173,8 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "597335a4a1eda7799303e0d930b5468af2364f5d2075aab48787b93e5774336d"
|
||||
"hash": "6bb9e6cab7034bf28ef2597445dc617b60c49b42568fdb975cf2d2dc8702167e"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "85c116da4a43a5ade37bb9ab2660d206b722ec4192368599c3a5027f50a89c80"
|
||||
}
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -160,8 +155,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90"
|
||||
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -161,8 +156,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "96dc1cd308f3c21cfb50b88048054dc03f93e261d25969d66aa48e9d0502960f"
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "emails",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a00f3f18087326432c9114998e47cff4f78d1b28cdb8adc6b18b937e1cf142d1"
|
||||
}
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -159,8 +154,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "aa2800113a8a8805f47cdc1dd0f29d94c546fe531e7edd3e91da4978af5442fb"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND app_version IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "aee9f7f1d99910c669ef7224a6cc237ac7a0d7f5a24849bb62a6e1a8babd8ef9"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14, tag = $15\n WHERE path = $16 AND workspace_id = $17 RETURNING *",
|
||||
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 WHERE path = $15 AND workspace_id = $16 RETURNING *",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -145,7 +140,6 @@
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
@@ -174,9 +168,8 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "90435687152ccd37db7e0a6a5baeb558db50f5f96b1e8eff0b10e4398566f325"
|
||||
"hash": "b7ed211ec7611c994a49565e46e5893788bcf49b2c9357fbc45a8353a7c84f48"
|
||||
}
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -160,8 +155,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c10348d26e3e3dc9e345d9044db35db9906617eb9c98aaf58a55d9681ffb85e3"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c87db5bfb559c912e35415c9945009041a5f5a03eaf03fe6bf27eed406640b6a"
|
||||
}
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -160,8 +155,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dd74fa9468b5fe8c8ad657ded06076c11a78d0206af2af20685c691d4d9520bb"
|
||||
|
||||
26
backend/.sqlx/query-df8ebebb61194d9427ee922d037fc0f224dcb80d7097f4af0ebc8effd5e2205b.json
generated
Normal file
26
backend/.sqlx/query-df8ebebb61194d9427ee922d037fc0f224dcb80d7097f4af0ebc8effd5e2205b.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "emails",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "df8ebebb61194d9427ee922d037fc0f224dcb80d7097f4af0ebc8effd5e2205b"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NULL and app_version IS NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e517ebdbb5c81ea0cb7355aaea0c17ad9a45ddc77f236399b453f9ddb93acecd"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin, email, job)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7, $8)",
|
||||
"query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin, email)\n VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,11 +11,10 @@
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Uuid"
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "42cb4bc3abc8cba2193a7fbbae8275c1bc830081a43e3a4a897c41b6b3099b1d"
|
||||
"hash": "e9c0e331c16312bf086b17c91466c5389d41454fd3f18d73c2e9554845ee9a72"
|
||||
}
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -160,8 +155,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "eced0a09ba547ce1dccb54a5419b22373603c9d01f77047b3553bde125bf71e8"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "emails",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ef0f4447498a117e4495ed9335d803403ad0055efb6da4bdd467b4ac9bf4e478"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO account (workspace_id, client, expires_at, refresh_token) VALUES ($1, $2, now() + ($3 || ' seconds')::interval, $4) RETURNING id",
|
||||
"query": "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5) RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,6 +11,7 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
@@ -21,5 +22,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dc649111c4c862c36c26ad8a9eff8c31c2533a4a73184d0276f9a388ff2fa574"
|
||||
"hash": "fa567c205929f41799a64aefac97c4504c7993b22478530e9345b9bc117e92e9"
|
||||
}
|
||||
@@ -122,11 +122,6 @@
|
||||
"ordinal": 23,
|
||||
"name": "no_flow_overlap",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "tag",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -158,8 +153,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fdd3710a381dac33ef5ee5ec5564a4874e6d7807d43fa6ea7b8408cad9e97480"
|
||||
|
||||
807
backend/Cargo.lock
generated
807
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.246.10"
|
||||
version = "1.236.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -22,7 +22,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.246.10"
|
||||
version = "1.236.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -37,7 +37,6 @@ incremental = true
|
||||
[features]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise"]
|
||||
enterprise_saml = ["windmill-api/enterprise_saml"]
|
||||
stripe = ["windmill-api/stripe"]
|
||||
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark"]
|
||||
flamegraph = ["windmill-common/flamegraph", "windmill-worker/flamegraph"]
|
||||
loki = ["windmill-common/loki"]
|
||||
@@ -69,7 +68,7 @@ uuid.workspace = true
|
||||
gethostname.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false, features = ['rt_tokio']}
|
||||
pg-embed = {git = "https://github.com/faokunega/pg-embed", optional = true, default-features = false}
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
@@ -189,7 +188,7 @@ rsmq_async = { version = "5.1.5" }
|
||||
gosyn = "0.2.6"
|
||||
bytes = "1.4.0"
|
||||
gethostname = "0.4.3"
|
||||
wasm-bindgen = "=0.2.89"
|
||||
wasm-bindgen = "0.2"
|
||||
serde-wasm-bindgen = "0.4"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
convert_case = "0.6.0"
|
||||
@@ -216,4 +215,3 @@ aws-config = "1.0.3"
|
||||
polars = { version = "0.35.2", features = ["lazy", "parquet", "aws", "csv", "dtype-full"] }
|
||||
polars-io = { version = "0.35.2", features = ["csv"] }
|
||||
object_store = { version = "0.8.0", features = ["aws"] }
|
||||
openidconnect = { version = "3.4.0" }
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE token ADD COLUMN IF NOT EXISTS job UUID;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
UPDATE workspace_settings SET git_sync = git_sync->0;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
UPDATE workspace_settings SET git_sync = '[]'::jsonb || git_sync
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE schedule DROP COLUMN tag;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE schedule ADD COLUMN tag VARCHAR(50);
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Add up migration script here
|
||||
|
||||
DROP POLICY IF EXISTS see_own ON account;
|
||||
DROP POLICY IF EXISTS see_member ON account;
|
||||
DROP POLICY IF EXISTS see_folder_extra_perms_user on account;
|
||||
ALTER TABLE account DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE account DROP COLUMN IF EXISTS owner;
|
||||
|
||||
GRANT ALL ON account TO windmill_admin;
|
||||
GRANT ALL ON account TO windmill_user;
|
||||
@@ -13,4 +13,4 @@ windmill-parser.workspace = true
|
||||
rustpython-parser.workspace = true
|
||||
itertools.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -93,7 +93,22 @@ pub fn parse_python_signature(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
.as_arg()
|
||||
.annotation
|
||||
.as_ref()
|
||||
.map_or(Typ::Unknown, |e| parse_expr(e));
|
||||
.map_or(Typ::Unknown, |e| match e.as_ref() {
|
||||
Expr::Name(ExprName { id, .. }) => match id.as_ref() {
|
||||
"str" => Typ::Str(None),
|
||||
"float" => Typ::Float,
|
||||
"int" => Typ::Int,
|
||||
"bool" => Typ::Bool,
|
||||
"dict" => Typ::Object(vec![]),
|
||||
"list" => Typ::List(Box::new(Typ::Str(None))),
|
||||
"bytes" => Typ::Bytes,
|
||||
"datetime" => Typ::Datetime,
|
||||
"datetime.datetime" => Typ::Datetime,
|
||||
"Sql" | "sql" => Typ::Sql,
|
||||
_ => Typ::Resource(id.to_string()),
|
||||
},
|
||||
_ => Typ::Unknown,
|
||||
});
|
||||
|
||||
if typ == Typ::Unknown
|
||||
&& default.is_some()
|
||||
@@ -119,58 +134,6 @@ pub fn parse_python_signature(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_expr(e: &Box<Expr>) -> Typ {
|
||||
match e.as_ref() {
|
||||
Expr::Name(ExprName { id, .. }) => parse_typ(id.as_ref()),
|
||||
Expr::Subscript(x) => match x.value.as_ref() {
|
||||
Expr::Name(ExprName { id, .. }) => match id.as_str() {
|
||||
"Literal" => {
|
||||
let values = match x.slice.as_ref() {
|
||||
Expr::Tuple(elts) => {
|
||||
let v: Vec<String> = elts
|
||||
.elts
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
Expr::Constant(c) => c.value.as_str().map(|x| x.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
if v.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Typ::Str(values)
|
||||
}
|
||||
"List" => Typ::List(Box::new(parse_expr(&x.slice))),
|
||||
_ => Typ::Unknown,
|
||||
},
|
||||
_ => Typ::Unknown,
|
||||
},
|
||||
_ => Typ::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_typ(id: &str) -> Typ {
|
||||
match id {
|
||||
"str" => Typ::Str(None),
|
||||
"float" => Typ::Float,
|
||||
"int" => Typ::Int,
|
||||
"bool" => Typ::Bool,
|
||||
"dict" => Typ::Object(vec![]),
|
||||
"list" => Typ::List(Box::new(Typ::Str(None))),
|
||||
"bytes" => Typ::Bytes,
|
||||
"datetime" => Typ::Datetime,
|
||||
"datetime.datetime" => Typ::Datetime,
|
||||
"Sql" | "sql" => Typ::Sql,
|
||||
_ => Typ::Resource(id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value<R>(et: &Expr<R>) -> Option<serde_json::Value> {
|
||||
match et {
|
||||
Expr::Constant(ExprConstant { value, .. }) => Some(constant_to_value(value)),
|
||||
@@ -403,44 +366,4 @@ def main(test1: str,
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_python_sig_4() -> anyhow::Result<()> {
|
||||
let code = r#"
|
||||
|
||||
import os
|
||||
|
||||
def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): return
|
||||
|
||||
"#;
|
||||
//println!("{}", serde_json::to_string()?);
|
||||
assert_eq!(
|
||||
parse_python_signature(code)?,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "test1".to_string(),
|
||||
typ: Typ::Str(Some(vec!["foo".to_string(), "bar".to_string()])),
|
||||
default: None,
|
||||
has_default: false
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "test2".to_string(),
|
||||
typ: Typ::List(Box::new(Typ::Str(Some(vec![
|
||||
"foo".to_string(),
|
||||
"bar".to_string()
|
||||
])))),
|
||||
default: None,
|
||||
has_default: false
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,5 +22,4 @@ swc_ecma_visit.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
convert_case.workspace = true
|
||||
regex.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
@@ -284,15 +284,12 @@ fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String
|
||||
(id.sym.to_string(), typ, nullable)
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE_SNK_CASE: Regex = Regex::new(r"_(\d)").unwrap();
|
||||
}
|
||||
|
||||
fn to_snake_case(s: &str) -> String {
|
||||
let r = s.to_case(Case::Snake);
|
||||
|
||||
// s_3 => s3
|
||||
RE_SNK_CASE.replace_all(&r, "$1").to_string()
|
||||
let re = Regex::new(r"_(\d)").unwrap();
|
||||
re.replace_all(&r, "$1").to_string()
|
||||
}
|
||||
|
||||
fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eou pipefail
|
||||
|
||||
deno task wasmbuild --out ../../../cli/wasm/
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"tasks": {
|
||||
"wasmbuild": "deno run -A https://deno.land/x/wasmbuild@0.15.4/main.ts"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"collaborators": [
|
||||
"Ruben Fiszel <ruben@windmill.dev>"
|
||||
],
|
||||
"version": "1.237.0",
|
||||
"version": "1.226.9",
|
||||
"files": [
|
||||
"windmill_parser_wasm_bg.wasm",
|
||||
"windmill_parser_wasm.js",
|
||||
@@ -14,4 +14,4 @@
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,10 +538,6 @@ async function __wbg_load(module, imports) {
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_eval_ff4183ac1495b791 = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
@@ -593,6 +589,10 @@ function __wbg_get_imports() {
|
||||
const ret = getObject(arg0) in getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_eval_596393dc5ae50a1b = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
|
||||
const ret = getObject(arg0) == getObject(arg1);
|
||||
return ret;
|
||||
|
||||
Binary file not shown.
@@ -22,12 +22,11 @@ use tokio::{
|
||||
use windmill_api::HTTP_CLIENT;
|
||||
use windmill_common::{
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CUSTOM_TAGS_SETTING,
|
||||
DISABLE_STATS_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING,
|
||||
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, DISABLE_STATS_SETTING, ENV_SETTINGS,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING,
|
||||
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
},
|
||||
stats::schedule_stats,
|
||||
utils::{rd_string, Mode},
|
||||
@@ -43,10 +42,9 @@ use windmill_worker::{
|
||||
|
||||
use crate::monitor::{
|
||||
initial_load, load_keep_job_dir, load_require_preexisting_user, monitor_db, monitor_pool,
|
||||
reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_extra_pip_index_url_setting, reload_job_default_timeout_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_retention_period_setting, reload_server_config,
|
||||
reload_worker_config,
|
||||
reload_base_url_setting, reload_extra_pip_index_url_setting,
|
||||
reload_job_default_timeout_setting, reload_license_key, reload_npm_config_registry_setting,
|
||||
reload_retention_period_setting, reload_server_config, reload_worker_config,
|
||||
};
|
||||
|
||||
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
@@ -428,9 +426,6 @@ Windmill Community Edition {GIT_VERSION}
|
||||
NPM_CONFIG_REGISTRY_SETTING => {
|
||||
reload_npm_config_registry_setting(&db).await
|
||||
},
|
||||
BUNFIG_INSTALL_SCOPES_SETTING => {
|
||||
reload_bunfig_install_scopes_setting(&db).await
|
||||
},
|
||||
KEEP_JOB_DIR_SETTING => {
|
||||
load_keep_job_dir(&db).await;
|
||||
},
|
||||
|
||||
@@ -21,9 +21,9 @@ use windmill_api::{
|
||||
use windmill_common::{
|
||||
error,
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, EXPOSE_DEBUG_METRICS_SETTING,
|
||||
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
BASE_URL_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING,
|
||||
},
|
||||
@@ -35,9 +35,8 @@ use windmill_common::{
|
||||
BASE_URL, DB, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
};
|
||||
use windmill_worker::{
|
||||
create_token_for_owner, handle_job_error, AuthedClient, SendResult, BUNFIG_INSTALL_SCOPES,
|
||||
JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL,
|
||||
SCRIPT_TOKEN_EXPIRY,
|
||||
create_token_for_owner, handle_job_error, AuthedClient, SendResult, JOB_DEFAULT_TIMEOUT,
|
||||
KEEP_JOB_DIR, NPM_CONFIG_REGISTRY, PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
|
||||
};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -139,9 +138,6 @@ pub async fn initial_load(
|
||||
if worker_mode {
|
||||
reload_npm_config_registry_setting(&db).await;
|
||||
}
|
||||
if worker_mode {
|
||||
reload_bunfig_install_scopes_setting(&db).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_metrics_enabled(db: &DB) -> error::Result<()> {
|
||||
@@ -306,33 +302,29 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
}
|
||||
|
||||
pub async fn reload_extra_pip_index_url_setting(db: &DB) {
|
||||
reload_option_setting_with_tracing(
|
||||
if let Err(e) = reload_option_setting(
|
||||
db,
|
||||
EXTRA_PIP_INDEX_URL_SETTING,
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
PIP_EXTRA_INDEX_URL.clone(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error reloading extra_pip_index_url period: {:?}", e)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_npm_config_registry_setting(db: &DB) {
|
||||
reload_option_setting_with_tracing(
|
||||
if let Err(e) = reload_option_setting(
|
||||
db,
|
||||
NPM_CONFIG_REGISTRY_SETTING,
|
||||
"NPM_CONFIG_REGISTRY",
|
||||
NPM_CONFIG_REGISTRY.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn reload_bunfig_install_scopes_setting(db: &DB) {
|
||||
reload_option_setting_with_tracing(
|
||||
db,
|
||||
BUNFIG_INSTALL_SCOPES_SETTING,
|
||||
"BUNFIG_INSTALL_SCOPES",
|
||||
BUNFIG_INSTALL_SCOPES.clone(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error reloading npm_config_registry period: {:?}", e)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_retention_period_setting(db: &DB) {
|
||||
@@ -351,13 +343,16 @@ pub async fn reload_retention_period_setting(db: &DB) {
|
||||
}
|
||||
|
||||
pub async fn reload_job_default_timeout_setting(db: &DB) {
|
||||
reload_option_setting_with_tracing(
|
||||
if let Err(e) = reload_option_setting(
|
||||
db,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
"JOB_DEFAULT_TIMEOUT_SECS",
|
||||
JOB_DEFAULT_TIMEOUT.clone(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error reloading job default timeout: {:?}", e)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_request_size(db: &DB) {
|
||||
@@ -405,16 +400,6 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_option_setting_with_tracing<T: FromStr + DeserializeOwned>(
|
||||
db: &DB,
|
||||
setting_name: &str,
|
||||
std_env_var: &str,
|
||||
lock: Arc<RwLock<Option<T>>>,
|
||||
) {
|
||||
if let Err(e) = reload_option_setting(db, setting_name, std_env_var, lock.clone()).await {
|
||||
tracing::error!("Error reloading setting {}: {:?}", setting_name, e)
|
||||
}
|
||||
}
|
||||
pub async fn reload_option_setting<T: FromStr + DeserializeOwned>(
|
||||
db: &DB,
|
||||
setting_name: &str,
|
||||
@@ -781,7 +766,6 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
"ephemeral-script",
|
||||
*SCRIPT_TOKEN_EXPIRY,
|
||||
&job.email,
|
||||
&job.id,
|
||||
)
|
||||
.await
|
||||
.expect("could not create job token");
|
||||
|
||||
@@ -34,12 +34,11 @@ pub async fn start() -> anyhow::Result<(String, PgEmbed)> {
|
||||
|
||||
pg.start_db().await.expect("pg start db");
|
||||
|
||||
//TODO: re-enable this to make it work
|
||||
// if !pg.database_exists("windmill").await.expect("db exists") {
|
||||
// pg.create_database("windmill")
|
||||
// .await
|
||||
// .expect("pg create database");
|
||||
// }
|
||||
if !pg.database_exists("windmill").await.expect("db exists") {
|
||||
pg.create_database("windmill")
|
||||
.await
|
||||
.expect("pg create database");
|
||||
}
|
||||
|
||||
let uri = pg.full_db_uri("windmill");
|
||||
Ok((uri, pg))
|
||||
|
||||
@@ -300,7 +300,7 @@ mod suspend_resume {
|
||||
let second = completed.next().await.unwrap();
|
||||
// print_job(second, &db).await;
|
||||
|
||||
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil()).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"
|
||||
))
|
||||
@@ -401,7 +401,7 @@ mod suspend_resume {
|
||||
/* ... and send a request resume it. */
|
||||
let second = completed.next().await.unwrap();
|
||||
|
||||
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil()).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}"
|
||||
))
|
||||
@@ -2873,7 +2873,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
|
||||
retry: None,
|
||||
no_flow_overlap: None,
|
||||
summary: None,
|
||||
tag: None,
|
||||
|
||||
};
|
||||
|
||||
let _ = client.create_schedule("test-workspace", &schedule).await;
|
||||
@@ -2936,8 +2936,7 @@ async fn test_script_schedule_handlers(db: Pool<Postgres>) {
|
||||
ws_error_handler_muted: None,
|
||||
retry: None,
|
||||
summary: None,
|
||||
no_flow_overlap: None,
|
||||
tag: None,
|
||||
no_flow_overlap: None
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -3017,7 +3016,6 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
|
||||
retry: None,
|
||||
no_flow_overlap: None,
|
||||
summary: None,
|
||||
tag: None,
|
||||
};
|
||||
|
||||
let _ = client.create_schedule("test-workspace", &schedule).await;
|
||||
@@ -3081,8 +3079,7 @@ async fn test_flow_schedule_handlers(db: Pool<Postgres>) {
|
||||
ws_error_handler_muted: None,
|
||||
retry: None,
|
||||
summary: None,
|
||||
no_flow_overlap: None,
|
||||
tag: None,
|
||||
no_flow_overlap: None
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -3148,7 +3145,7 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
|
||||
is_template: None,
|
||||
kind: None,
|
||||
parent_hash: None,
|
||||
lock: None,
|
||||
lock: vec![],
|
||||
summary: "".to_string(),
|
||||
tag: None,
|
||||
schema: std::collections::HashMap::new(),
|
||||
|
||||
@@ -9,8 +9,7 @@ name = "windmill_api"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "openidconnect"]
|
||||
stripe = ["async-stripe"]
|
||||
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
|
||||
enterprise_saml = ["samael"]
|
||||
benchmark = []
|
||||
|
||||
@@ -89,4 +88,3 @@ aws-sdk-s3.workspace = true
|
||||
polars.workspace = true
|
||||
polars-io.workspace = true
|
||||
object_store.workspace = true
|
||||
openidconnect = { workspace = true, optional = true}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
version: 1.246.4
|
||||
version: 1.233.0
|
||||
title: Windmill API
|
||||
contact:
|
||||
name: Windmill Team
|
||||
@@ -1525,19 +1525,17 @@ paths:
|
||||
s3_resource_path:
|
||||
type: string
|
||||
git_sync:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_16
|
||||
script_path:
|
||||
type: string
|
||||
git_repo_resource_path:
|
||||
type: string
|
||||
use_individual_branch:
|
||||
type: boolean
|
||||
required: &ref_17
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
type: object
|
||||
properties: &ref_16
|
||||
script_path:
|
||||
type: string
|
||||
git_repo_resource_path:
|
||||
type: string
|
||||
use_individual_branch:
|
||||
type: boolean
|
||||
required: &ref_17
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
required:
|
||||
- code_completion_enabled
|
||||
/w/{workspace}/workspaces/get_deploy_to:
|
||||
@@ -1893,11 +1891,9 @@ paths:
|
||||
type: object
|
||||
properties:
|
||||
git_sync_settings:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: *ref_16
|
||||
required: *ref_17
|
||||
type: object
|
||||
properties: *ref_16
|
||||
required: *ref_17
|
||||
responses:
|
||||
'200':
|
||||
description: status
|
||||
@@ -2086,29 +2082,6 @@ paths:
|
||||
- token_prefix
|
||||
- created_at
|
||||
- last_used_at
|
||||
/w/{workspace}/oidc/token/{audience}:
|
||||
post:
|
||||
summary: get OIDC token (ee only)
|
||||
operationId: getOidcToken
|
||||
tags:
|
||||
- oidc
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
- name: audience
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: new oidc token
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/w/{workspace}/variables/create:
|
||||
post:
|
||||
summary: create variable
|
||||
@@ -2553,10 +2526,13 @@ paths:
|
||||
type: string
|
||||
expires_in:
|
||||
type: integer
|
||||
owner:
|
||||
type: string
|
||||
client:
|
||||
type: string
|
||||
required:
|
||||
- expires_in
|
||||
- owner
|
||||
- client
|
||||
responses:
|
||||
'200':
|
||||
@@ -3378,32 +3354,32 @@ paths:
|
||||
id:
|
||||
type: string
|
||||
value:
|
||||
oneOf: &ref_206
|
||||
oneOf: &ref_204
|
||||
- type: object
|
||||
properties: &ref_190
|
||||
properties: &ref_188
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
oneOf: &ref_26
|
||||
- type: object
|
||||
properties: &ref_186
|
||||
properties: &ref_184
|
||||
value: {}
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- javascript
|
||||
required: &ref_187
|
||||
required: &ref_185
|
||||
- expr
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_188
|
||||
properties: &ref_186
|
||||
expr:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- javascript
|
||||
required: &ref_189
|
||||
required: &ref_187
|
||||
- expr
|
||||
- type
|
||||
discriminator: &ref_27
|
||||
@@ -3443,13 +3419,13 @@ paths:
|
||||
type: number
|
||||
concurrency_time_window_s:
|
||||
type: number
|
||||
required: &ref_191
|
||||
required: &ref_189
|
||||
- type
|
||||
- content
|
||||
- language
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_192
|
||||
properties: &ref_190
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3463,12 +3439,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
required: &ref_193
|
||||
required: &ref_191
|
||||
- type
|
||||
- path
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_194
|
||||
properties: &ref_192
|
||||
input_transforms:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3480,12 +3456,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- flow
|
||||
required: &ref_195
|
||||
required: &ref_193
|
||||
- type
|
||||
- path
|
||||
- input_transforms
|
||||
- type: object
|
||||
properties: &ref_196
|
||||
properties: &ref_194
|
||||
modules:
|
||||
type: array
|
||||
items:
|
||||
@@ -3507,13 +3483,13 @@ paths:
|
||||
type: boolean
|
||||
parallelism:
|
||||
type: integer
|
||||
required: &ref_197
|
||||
required: &ref_195
|
||||
- modules
|
||||
- iterator
|
||||
- skip_failures
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_198
|
||||
properties: &ref_196
|
||||
branches:
|
||||
type: array
|
||||
items:
|
||||
@@ -3544,12 +3520,12 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- branchone
|
||||
required: &ref_199
|
||||
required: &ref_197
|
||||
- branches
|
||||
- default
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_200
|
||||
properties: &ref_198
|
||||
branches:
|
||||
type: array
|
||||
items:
|
||||
@@ -3574,28 +3550,28 @@ paths:
|
||||
- branchall
|
||||
parallel:
|
||||
type: boolean
|
||||
required: &ref_201
|
||||
required: &ref_199
|
||||
- branches
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_202
|
||||
properties: &ref_200
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- identity
|
||||
flow:
|
||||
type: boolean
|
||||
required: &ref_203
|
||||
required: &ref_201
|
||||
- type
|
||||
- type: object
|
||||
properties: &ref_204
|
||||
properties: &ref_202
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- graphql
|
||||
required: &ref_205
|
||||
required: &ref_203
|
||||
- type
|
||||
discriminator: &ref_207
|
||||
discriminator: &ref_205
|
||||
propertyName: type
|
||||
mapping:
|
||||
rawscript: '#/components/schemas/RawScript'
|
||||
@@ -4335,7 +4311,9 @@ paths:
|
||||
is_template:
|
||||
type: boolean
|
||||
lock:
|
||||
type: string
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
@@ -6764,71 +6742,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
/w/{workspace}/jobs/run/dependencies:
|
||||
post:
|
||||
summary: run a one-off dependencies job
|
||||
operationId: runRawScriptDependencies
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_0
|
||||
requestBody:
|
||||
description: raw script content
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
raw_scripts:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: &ref_184
|
||||
raw_code:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
- python3
|
||||
- deno
|
||||
- go
|
||||
- bash
|
||||
- powershell
|
||||
- postgresql
|
||||
- mysql
|
||||
- bigquery
|
||||
- snowflake
|
||||
- mssql
|
||||
- graphql
|
||||
- nativets
|
||||
- bun
|
||||
required: &ref_185
|
||||
- raw_code
|
||||
- path
|
||||
- language
|
||||
entrypoint:
|
||||
type: string
|
||||
required:
|
||||
- entrypoint
|
||||
- raw_scripts
|
||||
responses:
|
||||
'201':
|
||||
description: dependency job result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
lock:
|
||||
type: string
|
||||
required:
|
||||
- lock
|
||||
/w/{workspace}/jobs/run/preview_flow:
|
||||
post:
|
||||
summary: run flow preview
|
||||
@@ -8333,8 +8246,6 @@ paths:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
required: &ref_148
|
||||
- path
|
||||
- schedule
|
||||
@@ -8404,8 +8315,6 @@ paths:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
required: &ref_150
|
||||
- schedule
|
||||
- timezone
|
||||
@@ -8551,8 +8460,6 @@ paths:
|
||||
type: string
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
tag:
|
||||
type: string
|
||||
required: &ref_80
|
||||
- path
|
||||
- edited_by
|
||||
@@ -8733,8 +8640,6 @@ paths:
|
||||
properties: &ref_82
|
||||
name:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
emails:
|
||||
type: array
|
||||
items:
|
||||
@@ -8761,141 +8666,6 @@ paths:
|
||||
type: object
|
||||
properties: *ref_82
|
||||
required: *ref_83
|
||||
/groups/create:
|
||||
post:
|
||||
summary: create instance group
|
||||
operationId: createInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
requestBody:
|
||||
description: create instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
responses:
|
||||
'200':
|
||||
description: instance group created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/groups/update/{name}:
|
||||
post:
|
||||
summary: update instance group
|
||||
operationId: updateInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_81
|
||||
requestBody:
|
||||
description: update instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
new_summary:
|
||||
type: string
|
||||
required:
|
||||
- new_summary
|
||||
responses:
|
||||
'200':
|
||||
description: instance group updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/groups/delete/{name}:
|
||||
delete:
|
||||
summary: delete instance group
|
||||
operationId: deleteInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_81
|
||||
responses:
|
||||
'200':
|
||||
description: instance group deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/groups/adduser/{name}:
|
||||
post:
|
||||
summary: add user to instance group
|
||||
operationId: addUserToInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_81
|
||||
requestBody:
|
||||
description: user to add to instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
responses:
|
||||
'200':
|
||||
description: user added to instance group
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/groups/removeuser/{name}:
|
||||
post:
|
||||
summary: remove user from instance group
|
||||
operationId: removeUserFromInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: *ref_81
|
||||
requestBody:
|
||||
description: user to remove from instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
responses:
|
||||
'200':
|
||||
description: user removed from instance group
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/w/{workspace}/groups/list:
|
||||
get:
|
||||
summary: list groups
|
||||
@@ -10295,7 +10065,7 @@ paths:
|
||||
schema: {}
|
||||
/w/{workspace}/job_helpers/list_stored_files:
|
||||
get:
|
||||
summary: List the file keys available in the workspace files storage (S3)
|
||||
summary: List the file keys available in the worspace files storage (S3)
|
||||
operationId: listStoredFiles
|
||||
tags:
|
||||
- helpers
|
||||
@@ -10525,8 +10295,6 @@ paths:
|
||||
type: boolean
|
||||
cancel_upload:
|
||||
type: boolean
|
||||
s3_resource_path:
|
||||
type: string
|
||||
required:
|
||||
- file_key
|
||||
- part_content_base64
|
||||
@@ -11307,60 +11075,56 @@ components:
|
||||
type: object
|
||||
properties: *ref_182
|
||||
required: *ref_183
|
||||
RawScriptForDependencies:
|
||||
StaticTransform:
|
||||
type: object
|
||||
properties: *ref_184
|
||||
required: *ref_185
|
||||
StaticTransform:
|
||||
JavascriptTransform:
|
||||
type: object
|
||||
properties: *ref_186
|
||||
required: *ref_187
|
||||
JavascriptTransform:
|
||||
type: object
|
||||
properties: *ref_188
|
||||
required: *ref_189
|
||||
InputTransform:
|
||||
oneOf: *ref_26
|
||||
discriminator: *ref_27
|
||||
RawScript:
|
||||
type: object
|
||||
properties: *ref_188
|
||||
required: *ref_189
|
||||
PathScript:
|
||||
type: object
|
||||
properties: *ref_190
|
||||
required: *ref_191
|
||||
PathScript:
|
||||
PathFlow:
|
||||
type: object
|
||||
properties: *ref_192
|
||||
required: *ref_193
|
||||
PathFlow:
|
||||
type: object
|
||||
properties: *ref_194
|
||||
required: *ref_195
|
||||
FlowModule:
|
||||
type: object
|
||||
properties: *ref_28
|
||||
required: *ref_29
|
||||
ForloopFlow:
|
||||
type: object
|
||||
properties: *ref_194
|
||||
required: *ref_195
|
||||
BranchOne:
|
||||
type: object
|
||||
properties: *ref_196
|
||||
required: *ref_197
|
||||
BranchOne:
|
||||
BranchAll:
|
||||
type: object
|
||||
properties: *ref_198
|
||||
required: *ref_199
|
||||
BranchAll:
|
||||
Identity:
|
||||
type: object
|
||||
properties: *ref_200
|
||||
required: *ref_201
|
||||
Identity:
|
||||
Graphql:
|
||||
type: object
|
||||
properties: *ref_202
|
||||
required: *ref_203
|
||||
Graphql:
|
||||
type: object
|
||||
properties: *ref_204
|
||||
required: *ref_205
|
||||
FlowModuleValue:
|
||||
oneOf: *ref_206
|
||||
discriminator: *ref_207
|
||||
oneOf: *ref_204
|
||||
discriminator: *ref_205
|
||||
Retry:
|
||||
type: object
|
||||
properties: *ref_78
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.246.10
|
||||
version: 1.236.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1193,9 +1193,7 @@ paths:
|
||||
large_file_storage:
|
||||
$ref: "#/components/schemas/LargeFileStorage"
|
||||
git_sync:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkspaceGitSync"
|
||||
$ref: "#/components/schemas/WorkspaceGitSync"
|
||||
required:
|
||||
- code_completion_enabled
|
||||
|
||||
@@ -1534,9 +1532,7 @@ paths:
|
||||
type: object
|
||||
properties:
|
||||
git_sync_settings:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkspaceGitSync"
|
||||
$ref: "#/components/schemas/WorkspaceGitSync"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
@@ -1681,29 +1677,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/TruncatedToken"
|
||||
|
||||
/w/{workspace}/oidc/token/{audience}:
|
||||
post:
|
||||
summary: get OIDC token (ee only)
|
||||
operationId: getOidcToken
|
||||
tags:
|
||||
- oidc
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: audience
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: new oidc token
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/variables/create:
|
||||
post:
|
||||
summary: create variable
|
||||
@@ -2021,10 +1994,13 @@ paths:
|
||||
type: string
|
||||
expires_in:
|
||||
type: integer
|
||||
owner:
|
||||
type: string
|
||||
client:
|
||||
type: string
|
||||
required:
|
||||
- expires_in
|
||||
- owner
|
||||
- client
|
||||
responses:
|
||||
"200":
|
||||
@@ -4640,45 +4616,6 @@ paths:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
/w/{workspace}/jobs/run/dependencies:
|
||||
post:
|
||||
summary: run a one-off dependencies job
|
||||
operationId: runRawScriptDependencies
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
|
||||
requestBody:
|
||||
description: raw script content
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
raw_scripts:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RawScriptForDependencies"
|
||||
entrypoint:
|
||||
type: string
|
||||
required:
|
||||
- entrypoint
|
||||
- raw_scripts
|
||||
responses:
|
||||
"201":
|
||||
description: dependency job result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
lock:
|
||||
type: string
|
||||
required:
|
||||
- lock
|
||||
|
||||
/w/{workspace}/jobs/run/preview_flow:
|
||||
post:
|
||||
summary: run flow preview
|
||||
@@ -5691,134 +5628,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InstanceGroup"
|
||||
|
||||
/groups/create:
|
||||
post:
|
||||
summary: create instance group
|
||||
operationId: createInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
requestBody:
|
||||
description: create instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
responses:
|
||||
"200":
|
||||
description: instance group created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/update/{name}:
|
||||
post:
|
||||
summary: update instance group
|
||||
operationId: updateInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Name"
|
||||
requestBody:
|
||||
description: update instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
new_summary:
|
||||
type: string
|
||||
required:
|
||||
- new_summary
|
||||
responses:
|
||||
"200":
|
||||
description: instance group updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/delete/{name}:
|
||||
delete:
|
||||
summary: delete instance group
|
||||
operationId: deleteInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
"200":
|
||||
description: instance group deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/adduser/{name}:
|
||||
post:
|
||||
summary: add user to instance group
|
||||
operationId: addUserToInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Name"
|
||||
requestBody:
|
||||
description: user to add to instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
responses:
|
||||
"200":
|
||||
description: user added to instance group
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/removeuser/{name}:
|
||||
post:
|
||||
summary: remove user from instance group
|
||||
operationId: removeUserFromInstanceGroup
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Name"
|
||||
requestBody:
|
||||
description: user to remove from instance group
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
responses:
|
||||
"200":
|
||||
description: user removed from instance group
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/groups/list:
|
||||
get:
|
||||
summary: list groups
|
||||
@@ -6909,7 +6718,7 @@ paths:
|
||||
|
||||
/w/{workspace}/job_helpers/list_stored_files:
|
||||
get:
|
||||
summary: List the file keys available in the workspace files storage (S3)
|
||||
summary: List the file keys available in the worspace files storage (S3)
|
||||
operationId: listStoredFiles
|
||||
tags:
|
||||
- helpers
|
||||
@@ -7084,8 +6893,6 @@ paths:
|
||||
type: boolean
|
||||
cancel_upload:
|
||||
type: boolean
|
||||
s3_resource_path:
|
||||
type: string
|
||||
required:
|
||||
- file_key
|
||||
- part_content_base64
|
||||
@@ -7625,7 +7432,9 @@ components:
|
||||
is_template:
|
||||
type: boolean
|
||||
lock:
|
||||
type: string
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
@@ -8644,8 +8453,6 @@ components:
|
||||
type: string
|
||||
no_flow_overlap:
|
||||
type: boolean
|
||||
tag:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- edited_by
|
||||
@@ -8719,8 +8526,6 @@ components:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- schedule
|
||||
@@ -8761,8 +8566,6 @@ components:
|
||||
type: boolean
|
||||
summary:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
required:
|
||||
- schedule
|
||||
- timezone
|
||||
@@ -8793,8 +8596,6 @@ components:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
emails:
|
||||
type: array
|
||||
items:
|
||||
@@ -9373,33 +9174,3 @@ components:
|
||||
required:
|
||||
- timestamp
|
||||
- value
|
||||
|
||||
RawScriptForDependencies:
|
||||
type: object
|
||||
properties:
|
||||
raw_code:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
powershell,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
snowflake,
|
||||
mssql,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
]
|
||||
required:
|
||||
- raw_code
|
||||
- path
|
||||
- language
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::collections::HashMap;
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
variables::build_crypt,
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
@@ -38,9 +39,7 @@ use windmill_common::{
|
||||
utils::{
|
||||
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, Pagination, StripPath,
|
||||
},
|
||||
variables::build_crypt,
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{push, PushArgs, PushIsolationLevel, QueueTransaction};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -588,18 +587,17 @@ async fn create_app(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
if let Some(dm) = app.deployment_message {
|
||||
args.insert("deployment_message".to_string(), json!(dm));
|
||||
}
|
||||
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
let (dependency_job_uuid, new_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::AppDependencies { path: app.path.clone(), version: v_id },
|
||||
args,
|
||||
JobPayload::AppDependencies {
|
||||
path: app.path.clone(),
|
||||
version: v_id,
|
||||
deployment_message: app.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
windmill_common::users::username_to_permissioned_as(&authed.username),
|
||||
@@ -665,9 +663,7 @@ pub async fn get_hub_app_by_id(
|
||||
|
||||
async fn delete_app(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
@@ -692,11 +688,10 @@ async fn delete_app(
|
||||
sqlx::query!(
|
||||
"DELETE FROM app WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
&w_id
|
||||
w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed.username,
|
||||
@@ -708,36 +703,6 @@ async fn delete_app(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::App {
|
||||
path: path.to_string(),
|
||||
parent_path: Some(path.to_string()),
|
||||
version: 0, // dummy version as it will not get inserted in db
|
||||
},
|
||||
Some(format!("App '{}' deleted", path)),
|
||||
rsmq,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND app_version IS NOT NULL",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error deleting deployment metadata for script with path {path} in workspace {w_id}: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone().clone(),
|
||||
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
|
||||
@@ -872,18 +837,16 @@ async fn update_app(
|
||||
let tx: PushIsolationLevel<'_, rsmq_async::MultiplexedRsmq> =
|
||||
PushIsolationLevel::Transaction(tx);
|
||||
if let Some(v_id) = v_id {
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
if let Some(dm) = ns.deployment_message {
|
||||
args.insert("deployment_message".to_string(), json!(dm));
|
||||
}
|
||||
args.insert("parent_path".to_string(), json!(path));
|
||||
|
||||
let (dependency_job_uuid, new_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::AppDependencies { path: npath.clone(), version: v_id },
|
||||
args,
|
||||
JobPayload::AppDependencies {
|
||||
path: npath.clone(),
|
||||
version: v_id,
|
||||
deployment_message: ns.deployment_message,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
windmill_common::users::username_to_permissioned_as(&authed.username),
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::{
|
||||
db::DB,
|
||||
@@ -26,7 +24,6 @@ use axum::{
|
||||
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sql_builder::prelude::*;
|
||||
use sql_builder::SqlBuilder;
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
@@ -41,7 +38,7 @@ use windmill_common::{
|
||||
scripts::Schema,
|
||||
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, StripPath},
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::PushArgs;
|
||||
use windmill_queue::{push, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -357,11 +354,6 @@ async fn create_flow(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
if let Some(dm) = nf.deployment_message {
|
||||
args.insert("deployment_message".to_string(), json!(dm));
|
||||
}
|
||||
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
let (dependency_job_uuid, mut new_tx) = push(
|
||||
&db,
|
||||
@@ -370,8 +362,9 @@ async fn create_flow(
|
||||
JobPayload::FlowDependencies {
|
||||
path: nf.path.clone(),
|
||||
dedicated_worker: nf.dedicated_worker,
|
||||
deployment_message: nf.deployment_message,
|
||||
},
|
||||
args,
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
windmill_common::users::username_to_permissioned_as(&authed.username),
|
||||
@@ -575,12 +568,6 @@ async fn update_flow(
|
||||
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
if let Some(dm) = nf.deployment_message {
|
||||
args.insert("deployment_message".to_string(), json!(dm));
|
||||
}
|
||||
args.insert("parent_path".to_string(), json!(flow_path));
|
||||
|
||||
let (dependency_job_uuid, mut new_tx) = push(
|
||||
&db,
|
||||
tx,
|
||||
@@ -588,8 +575,9 @@ async fn update_flow(
|
||||
JobPayload::FlowDependencies {
|
||||
path: nf.path.clone(),
|
||||
dedicated_worker: nf.dedicated_worker,
|
||||
deployment_message: nf.deployment_message,
|
||||
},
|
||||
args,
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
windmill_common::users::username_to_permissioned_as(&authed.username),
|
||||
@@ -720,10 +708,8 @@ struct Archived {
|
||||
|
||||
async fn archive_flow_by_path(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(archived): Json<Archived>,
|
||||
) -> Result<String> {
|
||||
@@ -750,27 +736,6 @@ async fn archive_flow_by_path(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Flow { path: path.to_string(), parent_path: Some(path.to_string()) },
|
||||
Some(format!(
|
||||
"Flow '{}' {}",
|
||||
path,
|
||||
if archived.archived.unwrap_or(true) {
|
||||
"archived"
|
||||
} else {
|
||||
"unarchived"
|
||||
}
|
||||
)),
|
||||
rsmq,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::ArchiveFlow { workspace: w_id, path: path.to_owned() },
|
||||
@@ -781,9 +746,7 @@ async fn archive_flow_by_path(
|
||||
|
||||
async fn delete_flow_by_path(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<String> {
|
||||
@@ -817,32 +780,6 @@ async fn delete_flow_by_path(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Flow { path: path.to_string(), parent_path: Some(path.to_string()) },
|
||||
Some(format!("Flow '{}' deleted", path)),
|
||||
rsmq,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NULL and app_version IS NULL",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error deleting deployment metadata for script with path {path} in workspace {w_id}: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteFlow { workspace: w_id, path: path.to_owned() },
|
||||
@@ -880,7 +817,6 @@ mod tests {
|
||||
)]
|
||||
.into(),
|
||||
hash: None,
|
||||
tag_override: None,
|
||||
},
|
||||
stop_after_if: None,
|
||||
summary: None,
|
||||
@@ -949,7 +885,6 @@ mod tests {
|
||||
path: "test".to_string(),
|
||||
input_transforms: HashMap::new(),
|
||||
hash: None,
|
||||
tag_override: None,
|
||||
},
|
||||
stop_after_if: Some(StopAfterIf {
|
||||
expr: "previous.isEmpty()".to_string(),
|
||||
@@ -985,8 +920,7 @@ mod tests {
|
||||
}
|
||||
},
|
||||
"type": "script",
|
||||
"path": "test",
|
||||
"tag_override": Option::<String>::None,
|
||||
"path": "test"
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1029,8 +963,7 @@ mod tests {
|
||||
"value": {
|
||||
"input_transforms": {},
|
||||
"type": "script",
|
||||
"path": "test",
|
||||
"tag_override": Option::<String>::None,
|
||||
"path": "test"
|
||||
},
|
||||
"stop_after_if": {
|
||||
"expr": "previous.isEmpty()",
|
||||
|
||||
@@ -43,7 +43,6 @@ pub fn global_service() -> Router {
|
||||
.route("/list", get(list_igroups))
|
||||
.route("/get/:name", get(get_igroup))
|
||||
.route("/create", post(create_igroup))
|
||||
.route("/update/:name", post(update_igroup))
|
||||
.route("/delete/:name", delete(delete_igroup))
|
||||
.route("/adduser/:name", post(add_user_igroup))
|
||||
.route("/removeuser/:name", post(remove_user_igroup))
|
||||
@@ -293,55 +292,14 @@ async fn create_igroup(
|
||||
Ok(format!("Created group {}", ng.name))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IGroupUpdate {
|
||||
new_summary: String,
|
||||
}
|
||||
|
||||
async fn update_igroup(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(name): Path<String>,
|
||||
Json(igroup_update): Json<IGroupUpdate>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
let exists_opt = sqlx::query("SELECT 1 FROM instance_group WHERE name = $1")
|
||||
.bind(name.clone())
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
not_found_if_none(exists_opt, "instance_group", name.clone())?;
|
||||
|
||||
sqlx::query("UPDATE instance_group SET summary = $1 WHERE name = $2")
|
||||
.bind(igroup_update.new_summary)
|
||||
.bind(&name)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed.username,
|
||||
"igroup.updated",
|
||||
ActionKind::Delete,
|
||||
"global",
|
||||
Some(&name.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(format!("Deleted group {}", name))
|
||||
}
|
||||
|
||||
async fn delete_igroup(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(name): Path<String>,
|
||||
Json(ng): Json<NewGroup>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
sqlx::query!("DELETE FROM instance_group WHERE name = $1", name)
|
||||
sqlx::query!("DELETE FROM instance_group WHERE name = $1", ng.name,)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -351,13 +309,13 @@ async fn delete_igroup(
|
||||
"igroup.delete",
|
||||
ActionKind::Delete,
|
||||
"global",
|
||||
Some(&name.to_string()),
|
||||
Some(&ng.name.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(format!("Deleted group {}", name))
|
||||
Ok(format!("Created group {}", ng.name))
|
||||
}
|
||||
|
||||
pub async fn get_group_opt<'c>(
|
||||
@@ -577,7 +535,6 @@ async fn add_user_igroup(
|
||||
#[derive(Serialize)]
|
||||
struct IGroup {
|
||||
name: String,
|
||||
summary: Option<String>,
|
||||
emails: Option<Vec<String>>,
|
||||
}
|
||||
async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
@@ -585,7 +542,7 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
|
||||
let groups = sqlx::query_as!(
|
||||
IGroup,
|
||||
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name"
|
||||
"SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name"
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
@@ -597,7 +554,7 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
async fn get_igroup(Path(name): Path<String>, Extension(db): Extension<DB>) -> JsonResult<IGroup> {
|
||||
let group = sqlx::query_as!(
|
||||
IGroup,
|
||||
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
|
||||
"SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
|
||||
name
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
|
||||
@@ -132,8 +132,8 @@ struct DuckdbConnectionSettingsQueryV2 {
|
||||
|
||||
async fn duckdb_connection_settings_v2(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Json(query): Json<DuckdbConnectionSettingsQueryV2>,
|
||||
@@ -142,15 +142,15 @@ async fn duckdb_connection_settings_v2(
|
||||
Some(s3_resource_path) => {
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&user_db,
|
||||
&db,
|
||||
Some(user_db),
|
||||
&token,
|
||||
&w_id,
|
||||
s3_resource_path.as_str(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?,
|
||||
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
|
||||
};
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
|
||||
"No datasets storage resource defined at the workspace level".to_string(),
|
||||
@@ -225,8 +225,8 @@ struct PolarsStorageOptions {
|
||||
|
||||
async fn polars_connection_settings_v2(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Json(query): Json<PolarsConnectionSettingsQueryV2>,
|
||||
@@ -235,15 +235,15 @@ async fn polars_connection_settings_v2(
|
||||
Some(s3_resource_path) => {
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&user_db,
|
||||
&db,
|
||||
Some(user_db),
|
||||
&token,
|
||||
&w_id,
|
||||
s3_resource_path.as_str(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?,
|
||||
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
|
||||
};
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
|
||||
"No datasets storage resource defined at the workspace level".to_string(),
|
||||
@@ -274,8 +274,8 @@ struct S3ResourceInfoQuery {
|
||||
|
||||
async fn s3_resource_info(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Json(query): Json<S3ResourceInfoQuery>,
|
||||
@@ -284,15 +284,15 @@ async fn s3_resource_info(
|
||||
Some(s3_resource_path) => {
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&user_db,
|
||||
&db,
|
||||
Some(user_db),
|
||||
&token,
|
||||
&w_id,
|
||||
s3_resource_path.as_str(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => get_workspace_s3_resource(&authed, &db, Some(user_db), &token, &w_id).await?,
|
||||
None => get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?,
|
||||
};
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::NotFound(
|
||||
"No datasets storage resource defined at the workspace level".to_string(),
|
||||
@@ -307,11 +307,12 @@ struct WindmillLargeFile {
|
||||
|
||||
async fn test_connection(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
) -> error::JsonResult<()> {
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
if s3_resource_opt.is_none() {
|
||||
return Err(error::Error::NotFound(
|
||||
"No datasets storage resource defined at the workspace level".to_string(),
|
||||
@@ -349,12 +350,13 @@ struct ListStoredDatasetsResponse {
|
||||
|
||||
async fn list_stored_files(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<ListStoredFilesQuery>,
|
||||
) -> error::JsonResult<ListStoredDatasetsResponse> {
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
@@ -462,13 +464,14 @@ enum WindmillContentType {
|
||||
|
||||
async fn load_file_metadata(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<LoadFileMetadataQuery>,
|
||||
) -> error::JsonResult<LoadFileMetadataResponse> {
|
||||
let file_key = query.file_key.clone();
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
@@ -502,6 +505,7 @@ async fn load_file_metadata(
|
||||
|
||||
async fn load_file_preview(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
@@ -515,7 +519,7 @@ async fn load_file_preview(
|
||||
}
|
||||
|
||||
let file_key = query.file_key.clone();
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
@@ -665,13 +669,14 @@ struct DeleteS3FileQuery {
|
||||
|
||||
async fn delete_s3_file(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<DeleteS3FileQuery>,
|
||||
) -> error::JsonResult<()> {
|
||||
let file_key = query.file_key.clone();
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
@@ -700,12 +705,13 @@ struct MoveS3FileQuery {
|
||||
|
||||
async fn move_s3_file(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<MoveS3FileQuery>,
|
||||
) -> error::JsonResult<()> {
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?;
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
@@ -749,8 +755,6 @@ struct UploadFileQuery {
|
||||
|
||||
pub is_final: bool, // whether it's the final chunk
|
||||
pub cancel_upload: bool, // whether the upload should be cancelled. upload_id should be set. subsequent calls with this upload_id will fail
|
||||
|
||||
pub s3_resource_path: Option<String>, // custom S3 resource to use for this upload. It None, the workspace S3 resource will be used
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
@@ -768,8 +772,8 @@ struct UploadFileResponse {
|
||||
|
||||
async fn multipart_upload_s3_file(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Tokened { token }: Tokened,
|
||||
Path(w_id): Path<String>,
|
||||
Json(query): Json<UploadFileQuery>,
|
||||
@@ -780,21 +784,7 @@ async fn multipart_upload_s3_file(
|
||||
query.is_final
|
||||
);
|
||||
let file_key = query.file_key.clone();
|
||||
|
||||
let s3_resource_opt = match query.s3_resource_path.clone() {
|
||||
Some(s3_resource_path) => {
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
&token,
|
||||
&w_id,
|
||||
s3_resource_path.as_str(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => get_workspace_s3_resource(&authed, &db, None, &token, &w_id).await?,
|
||||
};
|
||||
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &db, &token, &w_id).await?;
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
@@ -914,18 +904,20 @@ pub struct S3Object {
|
||||
|
||||
async fn get_workspace_s3_resource<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: &UserDB,
|
||||
db: &DB,
|
||||
user_db: Option<UserDB>,
|
||||
token: &str,
|
||||
w_id: &str,
|
||||
) -> error::Result<Option<S3Resource>> {
|
||||
let mut tx = user_db.clone().begin(authed).await?;
|
||||
let raw_lfs_opt = sqlx::query_scalar!(
|
||||
"SELECT large_file_storage FROM workspace_settings WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
tx.commit().await?;
|
||||
|
||||
if raw_lfs_opt.is_none() {
|
||||
return Ok(None);
|
||||
@@ -951,13 +943,13 @@ async fn get_workspace_s3_resource<'c>(
|
||||
Some(stripped) => stripped,
|
||||
None => s3_lfs.s3_resource_path.as_str(),
|
||||
};
|
||||
return get_s3_resource(authed, db, user_db, token, w_id, stripped_resource_path).await;
|
||||
return get_s3_resource(authed, user_db, db, token, w_id, stripped_resource_path).await;
|
||||
}
|
||||
|
||||
async fn get_s3_resource<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: &UserDB,
|
||||
db: &DB,
|
||||
user_db: Option<UserDB>,
|
||||
token: &str,
|
||||
w_id: &str,
|
||||
s3_resource_path: &str,
|
||||
|
||||
@@ -10,7 +10,6 @@ use axum::http::HeaderValue;
|
||||
use serde_json::value::RawValue;
|
||||
use std::collections::HashMap;
|
||||
use windmill_common::flow_status::RestartedFrom;
|
||||
use windmill_common::variables::get_workspace_key;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
@@ -18,6 +17,7 @@ use crate::{
|
||||
db::DB,
|
||||
users::{check_scopes, require_owner_of_path, OptAuthed},
|
||||
utils::require_super_admin,
|
||||
variables::get_workspace_key,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
@@ -152,7 +152,6 @@ pub fn workspaced_service() -> Router {
|
||||
"/result_by_id/:job_id/:node_id",
|
||||
get(get_result_by_id).layer(cors.clone()),
|
||||
)
|
||||
.route("/run/dependencies", post(run_dependencies_job))
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
@@ -1946,10 +1945,10 @@ pub struct WindmillCompositeResult {
|
||||
windmill_content_type: Option<String>,
|
||||
result: Option<Box<RawValue>>,
|
||||
}
|
||||
async fn run_wait_result(
|
||||
async fn run_wait_result<T>(
|
||||
db: &DB,
|
||||
uuid: Uuid,
|
||||
w_id: String,
|
||||
Path((w_id, _)): Path<(String, T)>,
|
||||
node_id_for_empty_return: Option<String>,
|
||||
) -> error::Result<Response> {
|
||||
let mut result;
|
||||
@@ -2184,7 +2183,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
|
||||
let wait_result = run_wait_result(&db, uuid, Path((w_id, script_path)), None).await;
|
||||
if delete_after_use.unwrap_or(false) {
|
||||
delete_job_metadata_after_use(&db, uuid).await?;
|
||||
}
|
||||
@@ -2303,7 +2302,7 @@ async fn run_wait_result_script_by_path_internal(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
|
||||
let wait_result = run_wait_result(&db, uuid, Path((w_id, script_path)), None).await;
|
||||
if delete_after_use.unwrap_or(false) {
|
||||
delete_job_metadata_after_use(&db, uuid).await?;
|
||||
}
|
||||
@@ -2378,7 +2377,7 @@ pub async fn run_wait_result_script_by_hash(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
|
||||
let wait_result = run_wait_result(&db, uuid, Path((w_id, script_hash)), None).await;
|
||||
if delete_after_use.unwrap_or(false) {
|
||||
delete_job_metadata_after_use(&db, uuid).await?;
|
||||
}
|
||||
@@ -2460,7 +2459,7 @@ async fn run_wait_result_flow_by_path_internal(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
run_wait_result(&db, uuid, w_id, early_return).await
|
||||
run_wait_result(&db, uuid, Path((w_id, flow_path)), early_return).await
|
||||
}
|
||||
|
||||
async fn run_preview_job(
|
||||
@@ -2528,82 +2527,6 @@ async fn run_preview_job(
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RunDependenciesRequest {
|
||||
pub raw_scripts: Vec<RawScriptForDependencies>,
|
||||
pub entrypoint: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct RawScriptForDependencies {
|
||||
pub script_path: String,
|
||||
pub raw_code: String,
|
||||
pub language: ScriptLang,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RunDependenciesResponse {
|
||||
pub dependencies: String,
|
||||
}
|
||||
|
||||
pub async fn run_dependencies_job(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(req): Json<RunDependenciesRequest>,
|
||||
) -> error::Result<Response> {
|
||||
check_scopes(&authed, || format!("runscript"))?;
|
||||
if authed.is_operator {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Operators cannot run dependencies jobs for security reasons".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if req.raw_scripts.len() != 1 || req.raw_scripts[0].script_path != req.entrypoint {
|
||||
return Err(error::Error::InternalErr(
|
||||
"For now only a single raw script can be passed to this endpoint, and the entrypoint should be set to the script path".to_string(),
|
||||
));
|
||||
}
|
||||
let raw_script = req.raw_scripts[0].clone();
|
||||
let script_path = raw_script.script_path;
|
||||
let raw_code = raw_script.raw_code;
|
||||
let language = raw_script.language;
|
||||
|
||||
let (uuid, tx) = push(
|
||||
&db,
|
||||
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq),
|
||||
&w_id,
|
||||
JobPayload::RawScriptDependencies {
|
||||
script_path: script_path,
|
||||
content: raw_code,
|
||||
language: language,
|
||||
},
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let wait_result = run_wait_result(&db, uuid, w_id, None).await;
|
||||
wait_result
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BatchInfo {
|
||||
kind: String,
|
||||
|
||||
@@ -63,8 +63,6 @@ pub mod job_helpers;
|
||||
pub mod job_metrics;
|
||||
pub mod jobs;
|
||||
pub mod oauth2;
|
||||
|
||||
mod oidc;
|
||||
mod openai;
|
||||
mod raw_apps;
|
||||
mod resources;
|
||||
@@ -214,8 +212,7 @@ pub async fn run_server(
|
||||
users::workspaced_service().layer(Extension(argon2.clone())),
|
||||
)
|
||||
.nest("/variables", variables::workspaced_service())
|
||||
.nest("/workspaces", workspaces::workspaced_service())
|
||||
.nest("/oidc", oidc::workspaced_service()),
|
||||
.nest("/workspaces", workspaces::workspaced_service()),
|
||||
)
|
||||
.nest("/workspaces", workspaces::global_service())
|
||||
.nest(
|
||||
@@ -238,7 +235,6 @@ pub async fn run_server(
|
||||
.route_layer(from_extractor::<ApiAuthed>())
|
||||
.route_layer(from_extractor::<users::Tokened>())
|
||||
.nest("/jobs", jobs::global_root_service())
|
||||
.nest("/oidc", oidc::global_service())
|
||||
.nest(
|
||||
"/saml",
|
||||
saml::global_service().layer(Extension(Arc::new(sp_extension.0))),
|
||||
|
||||
@@ -38,13 +38,16 @@ use windmill_common::jobs::JobPayload;
|
||||
use windmill_common::more_serde::maybe_number_opt;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::utils::{not_found_if_none, now_from_db};
|
||||
use windmill_common::variables::build_crypt;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::saml::SamlSsoLogin;
|
||||
use crate::users::{login_externally, LoginUserInfo};
|
||||
use crate::webhook_util::{InstanceEvent, WebhookShared};
|
||||
use crate::{db::DB, variables::encrypt, workspaces::WorkspaceSettings};
|
||||
use crate::{
|
||||
db::DB,
|
||||
variables::{build_crypt, encrypt},
|
||||
workspaces::WorkspaceSettings,
|
||||
};
|
||||
use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRET};
|
||||
use windmill_common::error::{self, to_anyhow, Error};
|
||||
use windmill_common::oauth2::*;
|
||||
@@ -382,6 +385,7 @@ async fn connect(
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccount {
|
||||
client: String,
|
||||
owner: String,
|
||||
refresh_token: Option<String>,
|
||||
expires_in: i64,
|
||||
}
|
||||
@@ -395,10 +399,11 @@ async fn create_account(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let id = sqlx::query_scalar!(
|
||||
"INSERT INTO account (workspace_id, client, expires_at, refresh_token) VALUES ($1, \
|
||||
$2, now() + ($3 || ' seconds')::interval, $4) RETURNING id",
|
||||
"INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, \
|
||||
$2, $3, now() + ($4 || ' seconds')::interval, $5) RETURNING id",
|
||||
w_id,
|
||||
payload.client,
|
||||
payload.owner,
|
||||
payload.expires_in.to_string(),
|
||||
payload.refresh_token
|
||||
)
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2023
|
||||
* 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::process::Command;
|
||||
|
||||
use anyhow;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use openidconnect::{
|
||||
core::{
|
||||
CoreClaimName, CoreJsonWebKeySet, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
||||
CoreResponseType, CoreRsaPrivateSigningKey, CoreSubjectIdentifierType,
|
||||
},
|
||||
AdditionalClaims, AuthUrl, EmptyAdditionalProviderMetadata, IssuerUrl, JsonWebKeyId,
|
||||
JsonWebKeySetUrl, ResponseTypes,
|
||||
};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
impl AdditionalClaims for JobClaim {}
|
||||
|
||||
use crate::db::DB;
|
||||
use axum::extract::Path;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Extension;
|
||||
use axum::{Json, Router};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/.well-known/openid-configuration",
|
||||
get(openid_configuration),
|
||||
)
|
||||
.route("/jwks", get(jwks))
|
||||
}
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/token/:audience", post(gen_token))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
struct Keys {
|
||||
private_key: String,
|
||||
}
|
||||
|
||||
async fn gen_pems(db: &DB) -> anyhow::Result<Keys> {
|
||||
let private_key_cmd = Command::new("openssl")
|
||||
.arg("genrsa")
|
||||
.arg("--traditional")
|
||||
.arg("2048")
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
let private_key = String::from_utf8(private_key_cmd.stdout).unwrap();
|
||||
|
||||
tracing::debug!("Generated private key: {}", private_key);
|
||||
let keys = Keys { private_key };
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO global_settings (name, value) VALUES ('rsa_keys', $1)",
|
||||
serde_json::to_value(&keys).unwrap()
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn get_private_key(db: &DB) -> anyhow::Result<String> {
|
||||
let key = sqlx::query_scalar!(
|
||||
"SELECT value->>'private_key' FROM global_settings WHERE name = 'rsa_keys'",
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if let Some(key) = key {
|
||||
return Ok(key);
|
||||
} else {
|
||||
let keys = gen_pems(db).await?;
|
||||
return Ok(keys.private_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn jwks(
|
||||
Extension(db): Extension<DB>,
|
||||
) -> windmill_common::error::JsonResult<CoreJsonWebKeySet> {
|
||||
use openidconnect::PrivateSigningKey;
|
||||
|
||||
let private_key = get_private_key(&db).await?;
|
||||
let jwks = CoreJsonWebKeySet::new(vec![CoreRsaPrivateSigningKey::from_pem(
|
||||
&private_key,
|
||||
Some(JsonWebKeyId::new("windmill".to_string())),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse PEM: {}", e))?
|
||||
.as_verification_key()]);
|
||||
|
||||
Ok(Json(jwks))
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn openid_configuration() -> windmill_common::error::JsonResult<CoreProviderMetadata> {
|
||||
use windmill_common::BASE_URL;
|
||||
|
||||
let base_url = BASE_URL.read().await.clone();
|
||||
return get_provider_metadata(base_url)
|
||||
.map(Json)
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub fn get_provider_metadata(base_url: String) -> anyhow::Result<CoreProviderMetadata> {
|
||||
let provider_metadata = CoreProviderMetadata::new(
|
||||
IssuerUrl::new(format!("{base_url}/api/oidc/"))?,
|
||||
AuthUrl::new(format!("{base_url}/api/oidc/"))?,
|
||||
JsonWebKeySetUrl::new(format!("{base_url}/api/oidc/jwks"))?,
|
||||
vec![
|
||||
// Optional: support the implicit flow.
|
||||
ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]), // Other flows including hybrid flows may also be specified here.
|
||||
],
|
||||
vec![CoreSubjectIdentifierType::Public],
|
||||
vec![CoreJwsSigningAlgorithm::RsaSsaPssSha256],
|
||||
EmptyAdditionalProviderMetadata {},
|
||||
)
|
||||
// Recommended: specify the supported ID token claims.
|
||||
.set_claims_supported(Some(vec![
|
||||
// Providers may also define an enum instead of using CoreClaimName.
|
||||
CoreClaimName::new("sub".to_string()),
|
||||
CoreClaimName::new("aud".to_string()),
|
||||
CoreClaimName::new("email".to_string()),
|
||||
CoreClaimName::new("email_verified".to_string()),
|
||||
CoreClaimName::new("exp".to_string()),
|
||||
CoreClaimName::new("iat".to_string()),
|
||||
CoreClaimName::new("iss".to_string()),
|
||||
CoreClaimName::new("job_id".to_string()),
|
||||
CoreClaimName::new("path".to_string()),
|
||||
CoreClaimName::new("flow_path".to_string()),
|
||||
CoreClaimName::new("groups".to_string()),
|
||||
CoreClaimName::new("username".to_string()),
|
||||
CoreClaimName::new("workspace".to_string()),
|
||||
]));
|
||||
return Ok(provider_metadata);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
|
||||
struct JobClaim {
|
||||
job_id: String,
|
||||
path: Option<String>,
|
||||
flow_path: Option<String>,
|
||||
groups: Vec<String>,
|
||||
username: String,
|
||||
email: String,
|
||||
workspace: String,
|
||||
}
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::users::Tokened;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn gen_token(
|
||||
authed: ApiAuthed,
|
||||
token: Tokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, audience)): Path<(String, String)>,
|
||||
) -> windmill_common::error::Result<String> {
|
||||
use chrono::{Duration, Utc};
|
||||
use openidconnect::{
|
||||
core::{CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm},
|
||||
Audience, EndUserEmail, IdToken, IdTokenClaims, StandardClaims, SubjectIdentifier,
|
||||
};
|
||||
use windmill_queue::get_queued_job;
|
||||
|
||||
use crate::users::get_groups_for_user;
|
||||
|
||||
let private_key = get_private_key(&db).await?;
|
||||
|
||||
let username = authed.username;
|
||||
let email = authed.email;
|
||||
|
||||
let job_id = {
|
||||
let job = sqlx::query_scalar!("SELECT job FROM token WHERE token = $1", token.token)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten();
|
||||
if job.is_none() {
|
||||
return Err(anyhow::anyhow!("Token not found").into());
|
||||
} else {
|
||||
job.unwrap()
|
||||
}
|
||||
};
|
||||
let mut tx = db.begin().await?;
|
||||
let job = get_queued_job(job_id, &w_id, &mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let job = job.ok_or_else(|| anyhow::anyhow!("Queued job {} not found", job_id))?;
|
||||
let issue_url = crate::BASE_URL.read().await.clone();
|
||||
let flow_path = if let Some(uuid) = job.parent_job {
|
||||
sqlx::query_scalar!("SELECT script_path FROM queue WHERE id = $1", uuid)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let groups = get_groups_for_user(&w_id, &username, &email, &db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let id_token = IdToken::<
|
||||
JobClaim,
|
||||
CoreGenderClaim,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJwsSigningAlgorithm,
|
||||
CoreJsonWebKeyType,
|
||||
>::new(
|
||||
IdTokenClaims::<JobClaim, CoreGenderClaim>::new(
|
||||
// Specify the issuer URL for the OpenID Connect Provider.
|
||||
IssuerUrl::new(issue_url)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate IssueUrl: {}", e))?,
|
||||
// The audience is usually a single entry with the client ID of the client for whom
|
||||
// the ID token is intended. This is a required claim.
|
||||
vec![Audience::new(audience)],
|
||||
// The ID token expiration is usually much shorter than that of the access or refresh
|
||||
// tokens issued to clients.
|
||||
Utc::now() + Duration::hours(48),
|
||||
// The issue time is usually the current time.
|
||||
Utc::now(),
|
||||
// Set the standard claims defined by the OpenID Connect Core spec.
|
||||
StandardClaims::new(
|
||||
// Stable subject identifiers are recommended in place of e-mail addresses or other
|
||||
// potentially unstable identifiers. This is the only required claim.
|
||||
SubjectIdentifier::new(format!(
|
||||
"{}::{}::{}::{}",
|
||||
email,
|
||||
job.script_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| "no_path".to_string()),
|
||||
flow_path.clone().unwrap_or_else(|| "no_flow".to_string()),
|
||||
w_id
|
||||
)),
|
||||
)
|
||||
// Optional: specify the user's e-mail address. This should only be provided if the
|
||||
// client has been granted the 'profile' or 'email' scopes.
|
||||
.set_email(Some(EndUserEmail::new(job.email.clone())))
|
||||
// Optional: specify whether the provider has verified the user's e-mail address.
|
||||
.set_email_verified(Some(true)),
|
||||
// OpenID Connect Providers may supply custom claims by providing a struct that
|
||||
// implements the AdditionalClaims trait. This requires manually using the
|
||||
// generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias,
|
||||
// however.
|
||||
JobClaim {
|
||||
job_id: job_id.to_string(),
|
||||
path: job.script_path,
|
||||
flow_path,
|
||||
username: job.created_by,
|
||||
email: job.email,
|
||||
workspace: job.workspace_id,
|
||||
groups,
|
||||
},
|
||||
),
|
||||
// The private key used for signing the ID token. For confidential clients (those able
|
||||
// to maintain a client secret), a CoreHmacKey can also be used, in conjunction
|
||||
// with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an
|
||||
// HMAC-based signing algorithm, the UTF-8 representation of the client secret should
|
||||
// be used as the HMAC key.
|
||||
&CoreRsaPrivateSigningKey::from_pem(
|
||||
&private_key,
|
||||
Some(JsonWebKeyId::new("windmill".to_string())),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid private key: {}", e))?,
|
||||
// Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS*
|
||||
// signature algorithm.
|
||||
CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
|
||||
// When returning the ID token alongside an access token (e.g., in the Authorization Code
|
||||
// flow), it is recommended to pass the access token here to set the `at_hash` claim
|
||||
// automatically.
|
||||
None,
|
||||
// When returning the ID token alongside an authorization code (e.g., in the implicit
|
||||
// flow), it is recommended to pass the authorization code here to set the `c_hash` claim
|
||||
// automatically.
|
||||
None,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate token: {}", e))?;
|
||||
Ok(id_token.to_string())
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
variables::build_crypt,
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
|
||||
@@ -16,10 +17,7 @@ use magic_crypt::MagicCryptTrait;
|
||||
use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{to_anyhow, Error},
|
||||
variables::build_crypt,
|
||||
};
|
||||
use windmill_common::error::{to_anyhow, Error};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ async fn get_resource_value_interpolated(
|
||||
) -> JsonResult<Option<serde_json::Value>> {
|
||||
return get_resource_value_interpolated_internal(
|
||||
&authed,
|
||||
Some(user_db),
|
||||
&user_db,
|
||||
&db,
|
||||
w_id.as_str(),
|
||||
path.to_path(),
|
||||
@@ -416,14 +416,14 @@ use async_recursion::async_recursion;
|
||||
|
||||
pub async fn get_resource_value_interpolated_internal(
|
||||
authed: &ApiAuthed,
|
||||
user_db: Option<UserDB>, // if none, no permission will be checked to access the resource
|
||||
user_db: &UserDB,
|
||||
db: &DB,
|
||||
workspace: &str,
|
||||
path: &str,
|
||||
job_id: Option<Uuid>,
|
||||
token: &str,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let mut tx = authed_transaction_or_default(authed, user_db.clone(), db).await?;
|
||||
let mut tx = user_db.clone().begin(authed).await?;
|
||||
|
||||
let value_o = sqlx::query_scalar!(
|
||||
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
|
||||
@@ -440,16 +440,7 @@ pub async fn get_resource_value_interpolated_internal(
|
||||
let value = not_found_if_none(value_o, "Resource", path)?;
|
||||
if let Some(value) = value {
|
||||
Ok(Some(
|
||||
transform_json_value(
|
||||
authed,
|
||||
user_db.clone(),
|
||||
db,
|
||||
workspace,
|
||||
value,
|
||||
&job_id,
|
||||
token,
|
||||
)
|
||||
.await?,
|
||||
transform_json_value(authed, user_db, db, workspace, value, &job_id, token).await?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -459,7 +450,7 @@ pub async fn get_resource_value_interpolated_internal(
|
||||
#[async_recursion]
|
||||
pub async fn transform_json_value<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: Option<UserDB>, // if none, no permission will be checked to access the resources/variables
|
||||
user_db: &UserDB,
|
||||
db: &DB,
|
||||
workspace: &str,
|
||||
v: Value,
|
||||
@@ -469,19 +460,9 @@ pub async fn transform_json_value<'c>(
|
||||
match v {
|
||||
Value::String(y) if y.starts_with("$var:") => {
|
||||
let path = y.strip_prefix("$var:").unwrap();
|
||||
let tx: Transaction<'_, Postgres> =
|
||||
authed_transaction_or_default(authed, user_db.clone(), db).await?;
|
||||
let v = crate::variables::get_value_internal(
|
||||
tx,
|
||||
db,
|
||||
workspace,
|
||||
path,
|
||||
user_db
|
||||
.clone()
|
||||
.map(|_| authed.username.as_str())
|
||||
.unwrap_or("backend"),
|
||||
)
|
||||
.await?;
|
||||
let tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
|
||||
let v = crate::variables::get_value_internal(tx, db, workspace, path, &authed.username)
|
||||
.await?;
|
||||
Ok(Value::String(v))
|
||||
}
|
||||
Value::String(y) if y.starts_with("$res:") => {
|
||||
@@ -489,8 +470,7 @@ pub async fn transform_json_value<'c>(
|
||||
if path.split("/").count() < 2 {
|
||||
return Err(Error::InternalErr(format!("Invalid resource path: {path}")));
|
||||
}
|
||||
let mut tx: Transaction<'_, Postgres> =
|
||||
authed_transaction_or_default(authed, user_db.clone(), db).await?;
|
||||
let mut tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
|
||||
let v = sqlx::query_scalar!(
|
||||
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
@@ -501,13 +481,13 @@ pub async fn transform_json_value<'c>(
|
||||
tx.commit().await?;
|
||||
let v = not_found_if_none(v, "Resource", path)?;
|
||||
if let Some(v) = v {
|
||||
transform_json_value(authed, user_db.clone(), db, workspace, v, job_id, token).await
|
||||
transform_json_value(authed, user_db, db, workspace, v, job_id, token).await
|
||||
} else {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
}
|
||||
Value::String(y) if y.starts_with("$") && job_id.is_some() => {
|
||||
let mut tx = authed_transaction_or_default(authed, user_db.clone(), db).await?;
|
||||
let mut tx = user_db.clone().begin(authed).await?;
|
||||
let job = sqlx::query_as::<_, QueuedJob>(
|
||||
"SELECT * FROM queue WHERE id = $1 AND workspace_id = $2",
|
||||
)
|
||||
@@ -520,8 +500,7 @@ pub async fn transform_json_value<'c>(
|
||||
let job = not_found_if_none(job, "Job", job_id.unwrap().to_string())?;
|
||||
|
||||
let flow_path = if let Some(uuid) = job.parent_job {
|
||||
let mut tx: Transaction<'_, Postgres> =
|
||||
authed_transaction_or_default(authed, user_db.clone(), db).await?;
|
||||
let mut tx: Transaction<'_, Postgres> = user_db.clone().begin(authed).await?;
|
||||
let p = sqlx::query_scalar!("SELECT script_path FROM queue WHERE id = $1", uuid)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
@@ -544,8 +523,6 @@ pub async fn transform_json_value<'c>(
|
||||
flow_path,
|
||||
job.schedule_path.clone(),
|
||||
job.flow_step_id.clone(),
|
||||
job.root_job.map(|x| x.to_string()),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -562,8 +539,7 @@ pub async fn transform_json_value<'c>(
|
||||
for (a, b) in m.clone().into_iter() {
|
||||
m.insert(
|
||||
a.clone(),
|
||||
transform_json_value(authed, user_db.clone(), db, workspace, b, job_id, token)
|
||||
.await?,
|
||||
transform_json_value(authed, user_db, db, workspace, b, job_id, token).await?,
|
||||
);
|
||||
}
|
||||
Ok(Value::Object(m))
|
||||
@@ -572,18 +548,6 @@ pub async fn transform_json_value<'c>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn authed_transaction_or_default<'c>(
|
||||
authed: &ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
) -> sqlx::error::Result<Transaction<'c, Postgres>> {
|
||||
if let Some(user_db) = user_db {
|
||||
user_db.begin(authed).await
|
||||
} else {
|
||||
db.clone().begin().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_path_conflict<'c>(
|
||||
tx: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
|
||||
@@ -69,7 +69,6 @@ pub struct NewSchedule {
|
||||
pub on_recovery_extra_args: Option<serde_json::Value>,
|
||||
pub ws_error_handler_muted: Option<bool>,
|
||||
pub retry: Option<serde_json::Value>,
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -156,9 +155,9 @@ async fn create_schedule(
|
||||
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
|
||||
is_flow, args, enabled, email, on_failure, on_failure_times, on_failure_exact, \
|
||||
on_failure_extra_args, on_recovery, on_recovery_times, on_recovery_extra_args, \
|
||||
ws_error_handler_muted, retry, summary, no_flow_overlap, tag \
|
||||
ws_error_handler_muted, retry, summary, no_flow_overlap \
|
||||
) VALUES ( \
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22 \
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 \
|
||||
) RETURNING *",
|
||||
w_id,
|
||||
ns.path,
|
||||
@@ -181,7 +180,6 @@ async fn create_schedule(
|
||||
ns.retry,
|
||||
ns.summary,
|
||||
ns.no_flow_overlap.unwrap_or(false),
|
||||
ns.tag,
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
@@ -235,9 +233,8 @@ async fn edit_schedule(
|
||||
Schedule,
|
||||
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3, on_failure = $4, on_failure_times = $5, \
|
||||
on_failure_exact = $6, on_failure_extra_args = $7, on_recovery = $8, on_recovery_times = $9, \
|
||||
on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, \
|
||||
no_flow_overlap = $14, tag = $15
|
||||
WHERE path = $16 AND workspace_id = $17 RETURNING *",
|
||||
on_recovery_extra_args = $10, ws_error_handler_muted = $11, retry = $12, summary = $13, no_flow_overlap = $14 \
|
||||
WHERE path = $15 AND workspace_id = $16 RETURNING *",
|
||||
es.schedule,
|
||||
es.timezone,
|
||||
es.args,
|
||||
@@ -252,7 +249,6 @@ async fn edit_schedule(
|
||||
es.retry,
|
||||
es.summary,
|
||||
es.no_flow_overlap.unwrap_or(false),
|
||||
es.tag,
|
||||
path,
|
||||
w_id,
|
||||
)
|
||||
@@ -348,7 +344,6 @@ pub struct ScheduleWJobs {
|
||||
pub jobs: Option<Vec<serde_json::Value>>,
|
||||
pub summary: Option<String>,
|
||||
pub no_flow_overlap: bool,
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_schedule_with_jobs(
|
||||
@@ -697,7 +692,6 @@ pub struct EditSchedule {
|
||||
pub ws_error_handler_muted: Option<bool>,
|
||||
pub retry: Option<serde_json::Value>,
|
||||
pub no_flow_overlap: Option<bool>,
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn clear_schedule<'c>(
|
||||
|
||||
@@ -27,7 +27,7 @@ use sql_builder::prelude::*;
|
||||
use sql_builder::SqlBuilder;
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, HashMap},
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -47,7 +47,9 @@ use windmill_common::{
|
||||
},
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{self, schedule::push_scheduled_job, PushIsolationLevel, QueueTransaction};
|
||||
use windmill_queue::{
|
||||
self, schedule::push_scheduled_job, PushArgs, PushIsolationLevel, QueueTransaction,
|
||||
};
|
||||
|
||||
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
|
||||
|
||||
@@ -456,6 +458,8 @@ async fn create_script(
|
||||
Some(String::new())
|
||||
} else {
|
||||
ns.lock
|
||||
.as_ref()
|
||||
.map(|x| x.join("\n"))
|
||||
.and_then(|e| if e.is_empty() { None } else { Some(e) })
|
||||
};
|
||||
|
||||
@@ -505,8 +509,7 @@ async fn create_script(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
let p_path_opt = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone());
|
||||
if let Some(ref p_path) = p_path_opt {
|
||||
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,
|
||||
@@ -608,15 +611,6 @@ async fn create_script(
|
||||
} else {
|
||||
ns.tag
|
||||
};
|
||||
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
if let Some(dm) = ns.deployment_message {
|
||||
args.insert("deployment_message".to_string(), json!(dm));
|
||||
}
|
||||
if let Some(ref p_path) = p_path_opt {
|
||||
args.insert("parent_path".to_string(), json!(p_path));
|
||||
}
|
||||
|
||||
let tx = PushIsolationLevel::Transaction(tx);
|
||||
let (_, new_tx) = windmill_queue::push(
|
||||
&db,
|
||||
@@ -627,8 +621,9 @@ async fn create_script(
|
||||
language: ns.language,
|
||||
path: ns.path,
|
||||
dedicated_worker: ns.dedicated_worker,
|
||||
deployment_message: ns.deployment_message,
|
||||
},
|
||||
args,
|
||||
PushArgs::empty(),
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
permissioned_as,
|
||||
@@ -654,14 +649,9 @@ async fn create_script(
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script {
|
||||
hash: hash.clone(),
|
||||
path: script_path.clone(),
|
||||
parent_path: p_path_opt,
|
||||
},
|
||||
DeployedObject::Script { hash: hash, path: script_path },
|
||||
ns.deployment_message,
|
||||
rsmq,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -1011,7 +1001,6 @@ async fn archive_script_by_path(
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> Result<()> {
|
||||
let path = path.to_path();
|
||||
@@ -1038,23 +1027,6 @@ async fn archive_script_by_path(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script {
|
||||
hash: ScriptHash(0), // dummy hash as it will not get inserted in db
|
||||
path: path.to_string(),
|
||||
parent_path: Some(path.to_string()),
|
||||
},
|
||||
Some(format!("Script '{}' archived", path)),
|
||||
rsmq,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
|
||||
@@ -1144,7 +1116,6 @@ async fn delete_script_by_path(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<String> {
|
||||
let path = path.to_path();
|
||||
@@ -1198,35 +1169,6 @@ async fn delete_script_by_path(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script {
|
||||
hash: ScriptHash(0), // Temporary value as it will get removed right after
|
||||
path: path.to_string(),
|
||||
parent_path: Some(path.to_string()),
|
||||
},
|
||||
Some(format!("Script '{}' deleted", path)),
|
||||
rsmq,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM deployment_metadata WHERE path = $1 AND workspace_id = $2 AND script_hash IS NOT NULL",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error deleting deployment metadata for script with path {path} in workspace {w_id}: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteScriptPath { workspace: w_id, path: path.to_string() },
|
||||
|
||||
@@ -25,9 +25,7 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult, Result},
|
||||
utils::{not_found_if_none, StripPath},
|
||||
variables::{
|
||||
build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable,
|
||||
},
|
||||
variables::{get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable},
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
@@ -69,8 +67,6 @@ async fn list_contextual_variables(
|
||||
Some("u/user/encapsulating_flow_path".to_string()),
|
||||
Some("u/user/triggering_flow_path".to_string()),
|
||||
Some("c".to_string()),
|
||||
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
|
||||
Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c".to_string()),
|
||||
)
|
||||
.await
|
||||
.to_vec(),
|
||||
@@ -196,6 +192,57 @@ async fn get_value(
|
||||
.map(Json);
|
||||
}
|
||||
|
||||
pub async fn get_value_internal<'c>(
|
||||
mut tx: Transaction<'c, Postgres>,
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
username: &str,
|
||||
) -> Result<String> {
|
||||
let variable_o = sqlx::query!(
|
||||
"SELECT value, account, (now() > account.expires_at) as is_expired, is_secret, path from variable
|
||||
LEFT JOIN account ON variable.account = account.id WHERE variable.path = $1 AND variable.workspace_id = $2", path, w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let variable = if let Some(variable) = variable_o {
|
||||
variable
|
||||
} else {
|
||||
explain_variable_perm_error(path, w_id, db).await?;
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let r = if variable.is_secret {
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
username,
|
||||
"variables.decrypt_secret",
|
||||
ActionKind::Execute,
|
||||
&w_id,
|
||||
Some(&variable.path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let value = variable.value;
|
||||
if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
|
||||
_refresh_token(tx, &variable.path, &w_id, variable.account.unwrap()).await?
|
||||
} else if !value.is_empty() {
|
||||
let mc = build_crypt(&mut tx, &w_id).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
mc.decrypt_base64_to_string(value)
|
||||
.map_err(|e| Error::InternalErr(e.to_string()))?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
variable.value
|
||||
};
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn explain_variable_perm_error(
|
||||
path: &str,
|
||||
w_id: &str,
|
||||
@@ -557,55 +604,31 @@ fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_value_internal<'c>(
|
||||
mut tx: Transaction<'c, Postgres>,
|
||||
db: &DB,
|
||||
pub async fn build_crypt<'c>(
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
username: &str,
|
||||
) -> Result<MagicCrypt256> {
|
||||
let key = get_workspace_key(w_id, db).await?;
|
||||
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
|
||||
format!("{}{}", key, salt)
|
||||
} else {
|
||||
key
|
||||
};
|
||||
Ok(magic_crypt::new_magic_crypt!(crypt_key, 256))
|
||||
}
|
||||
|
||||
pub async fn get_workspace_key<'c>(
|
||||
w_id: &str,
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
) -> Result<String> {
|
||||
let variable_o = sqlx::query!(
|
||||
"SELECT value, account, (now() > account.expires_at) as is_expired, is_secret, path from variable
|
||||
LEFT JOIN account ON variable.account = account.id WHERE variable.path = $1 AND variable.workspace_id = $2", path, w_id
|
||||
let key = sqlx::query_scalar!(
|
||||
"SELECT key FROM workspace_key WHERE workspace_id = $1 AND kind = 'cloud'",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let variable = if let Some(variable) = variable_o {
|
||||
variable
|
||||
} else {
|
||||
explain_variable_perm_error(path, w_id, db).await?;
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let r = if variable.is_secret {
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
username,
|
||||
"variables.decrypt_secret",
|
||||
ActionKind::Execute,
|
||||
&w_id,
|
||||
Some(&variable.path),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let value = variable.value;
|
||||
if variable.is_expired.unwrap_or(false) && variable.account.is_some() {
|
||||
_refresh_token(tx, &variable.path, &w_id, variable.account.unwrap()).await?
|
||||
} else if !value.is_empty() {
|
||||
let mc = build_crypt(&mut tx, &w_id).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
mc.decrypt_base64_to_string(value)
|
||||
.map_err(|e| Error::InternalErr(e.to_string()))?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
variable.value
|
||||
};
|
||||
|
||||
Ok(r)
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("fetching workspace key: {e}")))?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
@@ -18,9 +18,10 @@ use crate::{
|
||||
resources::{Resource, ResourceType},
|
||||
users::{send_email_if_possible, WorkspaceInvite, VALID_USERNAME},
|
||||
utils::require_super_admin,
|
||||
variables::build_crypt,
|
||||
webhook_util::{InstanceEvent, WebhookShared},
|
||||
};
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
use axum::response::Redirect;
|
||||
use axum::{
|
||||
body::StreamBody,
|
||||
@@ -31,10 +32,10 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::Utc;
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
use chrono::{Datelike, TimeZone, Timelike};
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
use stripe::CustomerId;
|
||||
use uuid::Uuid;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
@@ -42,7 +43,6 @@ use windmill_common::db::UserDB;
|
||||
use windmill_common::s3_helpers::LargeFileStorage;
|
||||
use windmill_common::schedule::Schedule;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::variables::build_crypt;
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
use windmill_common::workspaces::WorkspaceGitRepo;
|
||||
use windmill_common::{
|
||||
@@ -83,6 +83,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/edit_deploy_to", post(edit_deploy_to))
|
||||
.route("/tarball", get(tarball_workspace))
|
||||
.route("/is_premium", get(is_premium))
|
||||
.route("/premium_info", get(premium_info))
|
||||
.route("/edit_copilot_config", post(edit_copilot_config))
|
||||
.route("/get_copilot_info", get(get_copilot_info))
|
||||
.route("/edit_error_handler", post(edit_error_handler))
|
||||
@@ -93,7 +94,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/edit_git_sync_config", post(edit_git_sync_config))
|
||||
.route("/leave", post(leave_workspace));
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
if STRIPE_KEY.is_none() {
|
||||
return router;
|
||||
@@ -101,13 +102,12 @@ pub fn workspaced_service() -> Router {
|
||||
tracing::info!("stripe enabled");
|
||||
|
||||
return router
|
||||
.route("/premium_info", get(premium_info))
|
||||
.route("/checkout", get(stripe_checkout))
|
||||
.route("/billing_portal", get(stripe_portal));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stripe"))]
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
router
|
||||
}
|
||||
pub fn global_service() -> Router {
|
||||
@@ -324,7 +324,6 @@ pub struct PremiumWorkspaceInfo {
|
||||
pub usage: Option<i32>,
|
||||
pub seats: Option<i32>,
|
||||
}
|
||||
#[cfg(feature = "stripe")]
|
||||
async fn premium_info(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -396,14 +395,14 @@ async fn premium_info(
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
struct PlanQuery {
|
||||
plan: String,
|
||||
seats: Option<i32>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn stripe_checkout(
|
||||
authed: ApiAuthed,
|
||||
Path(w_id): Path<String>,
|
||||
@@ -492,7 +491,7 @@ async fn stripe_checkout(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn stripe_portal(
|
||||
authed: ApiAuthed,
|
||||
Path(w_id): Path<String>,
|
||||
@@ -1044,7 +1043,7 @@ async fn edit_large_file_storage_config(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditGitSyncConfig {
|
||||
git_sync_settings: Option<Vec<WorkspaceGitRepo>>,
|
||||
git_sync_settings: Option<WorkspaceGitRepo>,
|
||||
}
|
||||
|
||||
async fn edit_git_sync_config(
|
||||
@@ -1078,7 +1077,7 @@ async fn edit_git_sync_config(
|
||||
.await?;
|
||||
|
||||
if let Some(git_sync_settings) = new_config.git_sync_settings {
|
||||
let serialized_config = serde_json::to_value::<Vec<WorkspaceGitRepo>>(git_sync_settings)
|
||||
let serialized_config = serde_json::to_value::<WorkspaceGitRepo>(git_sync_settings)
|
||||
.map_err(|err| Error::InternalErr(err.to_string()))?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -1813,7 +1812,7 @@ struct ScriptMetadata {
|
||||
description: String,
|
||||
schema: Option<Schema>,
|
||||
is_template: bool,
|
||||
lock: Option<String>,
|
||||
lock: Vec<String>,
|
||||
kind: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
envs: Option<Vec<String>>,
|
||||
@@ -2021,13 +2020,19 @@ async fn tarball_workspace(
|
||||
.write_to_archive(&script.content, &format!("{}.{}", script.path, ext))
|
||||
.await?;
|
||||
|
||||
let lock = script
|
||||
.lock
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.map(|x| x.to_string())
|
||||
.collect();
|
||||
let metadata = ScriptMetadata {
|
||||
summary: script.summary,
|
||||
description: script.description,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
kind: script.kind.to_string(),
|
||||
lock: script.lock,
|
||||
lock,
|
||||
envs: script.envs,
|
||||
concurrent_limit: script.concurrent_limit,
|
||||
concurrency_time_window_s: script.concurrency_time_window_s,
|
||||
|
||||
@@ -50,5 +50,4 @@ git-version.workspace = true
|
||||
cron.workspace = true
|
||||
tracing-loki = { version = "^0", optional = true }
|
||||
aws-sdk-s3.workspace = true
|
||||
aws-config.workspace = true
|
||||
magic-crypt.workspace = true
|
||||
aws-config.workspace = true
|
||||
@@ -96,9 +96,9 @@ impl IntoResponse for Error {
|
||||
};
|
||||
|
||||
if matches!(status, axum::http::StatusCode::NOT_FOUND) {
|
||||
tracing::warn!(message = e.to_string());
|
||||
tracing::warn!(not_found = e.to_string());
|
||||
} else {
|
||||
tracing::error!(essage = e.to_string());
|
||||
tracing::error!(error = e.to_string());
|
||||
};
|
||||
|
||||
axum::response::Response::builder()
|
||||
|
||||
@@ -292,7 +292,6 @@ pub enum FlowModuleValue {
|
||||
path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hash: Option<ScriptHash>,
|
||||
tag_override: Option<String>,
|
||||
},
|
||||
Flow {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -7,8 +7,6 @@ pub const JOB_DEFAULT_TIMEOUT_SECS_SETTING: &str = "job_default_timeout";
|
||||
pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb";
|
||||
pub const LICENSE_KEY_SETTING: &str = "license_key";
|
||||
pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry";
|
||||
pub const BUNFIG_INSTALL_SCOPES_SETTING: &str = "bunfig_install_scopes";
|
||||
|
||||
pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url";
|
||||
pub const UNIQUE_ID_SETTING: &str = "uid";
|
||||
pub const DISABLE_STATS_SETTING: &str = "disable_stats";
|
||||
|
||||
@@ -285,19 +285,17 @@ pub enum JobPayload {
|
||||
hash: ScriptHash,
|
||||
language: ScriptLang,
|
||||
dedicated_worker: Option<bool>,
|
||||
deployment_message: Option<String>,
|
||||
},
|
||||
FlowDependencies {
|
||||
path: String,
|
||||
dedicated_worker: Option<bool>,
|
||||
deployment_message: Option<String>,
|
||||
},
|
||||
AppDependencies {
|
||||
path: String,
|
||||
version: i64,
|
||||
},
|
||||
RawScriptDependencies {
|
||||
script_path: String,
|
||||
content: String,
|
||||
language: ScriptLang,
|
||||
deployment_message: Option<String>,
|
||||
},
|
||||
Flow {
|
||||
path: String,
|
||||
@@ -322,7 +320,6 @@ pub enum JobPayload {
|
||||
concurrency_time_window_s: Option<i32>,
|
||||
cache_ttl: Option<i32>,
|
||||
priority: Option<i16>,
|
||||
tag_override: Option<String>,
|
||||
},
|
||||
DeploymentCallback {
|
||||
path: String,
|
||||
|
||||
@@ -39,7 +39,6 @@ pub struct Schedule {
|
||||
pub retry: Option<serde_json::Value>,
|
||||
pub no_flow_overlap: bool,
|
||||
pub summary: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
|
||||
@@ -240,9 +240,7 @@ pub struct NewScript {
|
||||
pub content: String,
|
||||
pub schema: Option<Schema>,
|
||||
pub is_template: Option<bool>,
|
||||
#[serde(default = "Option::default")]
|
||||
#[serde(deserialize_with = "lock_deserialize")]
|
||||
pub lock: Option<String>,
|
||||
pub lock: Option<Vec<String>>,
|
||||
pub language: ScriptLang,
|
||||
pub kind: Option<ScriptKind>,
|
||||
pub tag: Option<String>,
|
||||
@@ -260,52 +258,6 @@ pub struct NewScript {
|
||||
pub deployment_message: Option<String>,
|
||||
}
|
||||
|
||||
fn lock_deserialize<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
struct StringOrArrayVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for StringOrArrayVisitor {
|
||||
type Value = Option<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("either a string or an array of strings")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(Some(v.to_string()))
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut split_lock: Vec<String> = vec![];
|
||||
loop {
|
||||
if let Ok(Some(elem)) = seq.next_element::<String>() {
|
||||
split_lock.push(elem);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let lock = split_lock.join("\n");
|
||||
return Ok(Some(lock));
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(StringOrArrayVisitor)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListScriptQuery {
|
||||
pub path_start: Option<String>,
|
||||
|
||||
@@ -6,15 +6,9 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use magic_crypt::{MagicCrypt256, MagicCryptTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
|
||||
use crate::{BASE_URL, DB};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SECRET_SALT: Option<String> = std::env::var("SECRET_SALT").ok();
|
||||
}
|
||||
use crate::BASE_URL;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
||||
@@ -66,72 +60,6 @@ pub struct CreateVariable {
|
||||
pub is_oauth: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn build_crypt<'c>(
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
) -> crate::error::Result<MagicCrypt256> {
|
||||
let key = get_workspace_key(w_id, db).await?;
|
||||
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
|
||||
format!("{}{}", key, salt)
|
||||
} else {
|
||||
key
|
||||
};
|
||||
Ok(magic_crypt::new_magic_crypt!(crypt_key, 256))
|
||||
}
|
||||
|
||||
pub async fn get_workspace_key<'c>(
|
||||
w_id: &str,
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
) -> crate::error::Result<String> {
|
||||
let key = sqlx::query_scalar!(
|
||||
"SELECT key FROM workspace_key WHERE workspace_id = $1 AND kind = 'cloud'",
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
.map_err(|e| crate::Error::InternalErr(format!("fetching workspace key: {e}")))?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub async fn get_secret_value_as_admin(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
) -> crate::error::Result<String> {
|
||||
let variable_o = sqlx::query!(
|
||||
"SELECT value, is_secret, path from variable WHERE variable.path = $1 AND variable.workspace_id = $2", path, w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let variable = if let Some(variable) = variable_o {
|
||||
variable
|
||||
} else {
|
||||
return Err(crate::Error::NotFound(format!(
|
||||
"variable {} not found in workspace {}",
|
||||
path, w_id
|
||||
)));
|
||||
};
|
||||
|
||||
let r = if variable.is_secret {
|
||||
let value = variable.value;
|
||||
if !value.is_empty() {
|
||||
let mut tx = db.begin().await?;
|
||||
let mc = build_crypt(&mut tx, &w_id).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
mc.decrypt_base64_to_string(value)
|
||||
.map_err(|e| crate::Error::InternalErr(e.to_string()))?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
variable.value
|
||||
};
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub async fn get_reserved_variables(
|
||||
w_id: &str,
|
||||
token: &str,
|
||||
@@ -144,9 +72,7 @@ pub async fn get_reserved_variables(
|
||||
flow_path: Option<String>,
|
||||
schedule_path: Option<String>,
|
||||
step_id: Option<String>,
|
||||
root_flow_id: Option<String>,
|
||||
jwt_token: Option<String>,
|
||||
) -> [ContextualVariable; 17] {
|
||||
) -> [ContextualVariable; 15] {
|
||||
let state_path = {
|
||||
let trigger = if schedule_path.is_some() {
|
||||
username.to_string()
|
||||
@@ -235,17 +161,11 @@ pub async fn get_reserved_variables(
|
||||
value: flow_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "Job id of the encapsulating flow if the job is a flow step".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_ROOT_FLOW_JOB_ID".to_string(),
|
||||
value: root_flow_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "Job id of the root flow if the job is a flow step".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_PATH".to_string(),
|
||||
value: flow_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the encapsulating flow if the job is a flow step".to_string(),
|
||||
},
|
||||
|
||||
ContextualVariable {
|
||||
name: "WM_SCHEDULE_PATH".to_string(),
|
||||
value: schedule_path.unwrap_or_else(|| "".to_string()),
|
||||
@@ -277,11 +197,6 @@ pub async fn get_reserved_variables(
|
||||
name: "WM_OBJECT_PATH".to_string(),
|
||||
value: object_path,
|
||||
description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(),
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_OIDC_JWT".to_string(),
|
||||
value: jwt_token.unwrap_or_else(|| "".to_string()),
|
||||
description: "OIDC JWT token (EE only)".to_string(),
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ enterprise = ["windmill-queue/enterprise"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
uuid.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
|
||||
use windmill_common::workspaces::WorkspaceGitRepo;
|
||||
|
||||
@@ -23,30 +22,22 @@ pub type DB = Pool<Postgres>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DeployedObject {
|
||||
Script { hash: ScriptHash, path: String, parent_path: Option<String> },
|
||||
Flow { path: String, parent_path: Option<String> },
|
||||
App { path: String, version: i64, parent_path: Option<String> },
|
||||
Script { hash: ScriptHash, path: String },
|
||||
Flow { path: String },
|
||||
App { path: String, version: i64 },
|
||||
}
|
||||
|
||||
impl DeployedObject {
|
||||
pub fn get_path(&self) -> &str {
|
||||
match self {
|
||||
DeployedObject::Script { path, .. } => path,
|
||||
DeployedObject::Flow { path, .. } => path,
|
||||
DeployedObject::Flow { path } => path,
|
||||
DeployedObject::App { path, .. } => path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_parent_path(&self) -> Option<String> {
|
||||
match self {
|
||||
DeployedObject::Script { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::Flow { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::App { parent_path, .. } => parent_path.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send + Clone + 'c>(
|
||||
pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
email: &str,
|
||||
created_by: &str,
|
||||
db: &DB,
|
||||
@@ -54,25 +45,8 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
obj: DeployedObject,
|
||||
deployment_message: Option<String>,
|
||||
rsmq: Option<R>,
|
||||
skip_db_insert: bool,
|
||||
) -> Result<()> {
|
||||
let exclude_path_prefix = "u/";
|
||||
let obj_path = if obj.get_path().starts_with(exclude_path_prefix) {
|
||||
None
|
||||
} else {
|
||||
Some(obj.get_path())
|
||||
};
|
||||
let obj_parent_path = if obj
|
||||
.get_parent_path()
|
||||
.unwrap_or(exclude_path_prefix.to_string())
|
||||
.starts_with(exclude_path_prefix)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
obj.get_parent_path()
|
||||
};
|
||||
|
||||
let skip_git_sync = if obj_path.is_none() && obj_parent_path.is_none() {
|
||||
let skip_git_sync = if obj.get_path().starts_with("u/") {
|
||||
tracing::debug!(
|
||||
"Ignoring {} from git sync as it's in a private user folder",
|
||||
obj.get_path()
|
||||
@@ -95,123 +69,118 @@ pub async fn handle_deployment_metadata<'c, R: rsmq_async::RsmqConnection + Send
|
||||
));
|
||||
}
|
||||
|
||||
let workspace_git_repos = workspace_git_repo_setting
|
||||
let workspace_git_repo = workspace_git_repo_setting
|
||||
.unwrap()
|
||||
.map(|conf| serde_json::from_value::<Vec<WorkspaceGitRepo>>(conf).ok())
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
.map(|conf| serde_json::from_value::<WorkspaceGitRepo>(conf).ok())
|
||||
.flatten();
|
||||
|
||||
let mut git_sync_job_uuids: Vec<Uuid> = vec![];
|
||||
if !skip_git_sync {
|
||||
for workspace_git_repo in workspace_git_repos {
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
args.insert(
|
||||
"repo_url_resource_path".to_string(),
|
||||
json!(workspace_git_repo
|
||||
.git_repo_resource_path
|
||||
.strip_prefix("$res:")),
|
||||
);
|
||||
let git_sync_job_uuid = if !skip_git_sync && workspace_git_repo.is_some() {
|
||||
let workspace_git_repo = workspace_git_repo.unwrap();
|
||||
|
||||
if let Some(path) = obj_path {
|
||||
args.insert("path".to_string(), json!(path));
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
args.insert(
|
||||
"repo_url_resource_path".to_string(),
|
||||
json!(workspace_git_repo
|
||||
.git_repo_resource_path
|
||||
.strip_prefix("$res:")),
|
||||
);
|
||||
|
||||
let message = match obj.clone() {
|
||||
DeployedObject::Script { path, .. } => {
|
||||
args.insert("path".to_string(), json!(path.to_string()));
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("Script '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
if let Some(parent_path) = obj_parent_path.clone() {
|
||||
args.insert("parent_path".to_string(), json!(parent_path));
|
||||
DeployedObject::Flow { path } => {
|
||||
args.insert("path".to_string(), json!(path.to_string()));
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("Flow '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
args.insert("workspace_id".to_string(), json!(w_id));
|
||||
|
||||
let message = match obj.clone() {
|
||||
DeployedObject::Script { path, .. } => {
|
||||
args.insert("path_type".to_string(), json!("script"));
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("Script '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
DeployedObject::App { path, .. } => {
|
||||
args.insert("path".to_string(), json!(path.to_string()));
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("App '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
DeployedObject::Flow { path, .. } => {
|
||||
args.insert("path_type".to_string(), json!("flow"));
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("Flow '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
DeployedObject::App { path, .. } => {
|
||||
args.insert("path_type".to_string(), json!("app"));
|
||||
if deployment_message.as_ref().is_none()
|
||||
|| deployment_message.as_ref().is_some_and(|x| x.is_empty())
|
||||
{
|
||||
format!("App '{}' deployed", path)
|
||||
} else {
|
||||
deployment_message.clone().unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
args.insert("commit_msg".to_string(), json!(message));
|
||||
args.insert(
|
||||
"use_individual_branch".to_string(),
|
||||
json!(workspace_git_repo.use_individual_branch.unwrap_or(false)),
|
||||
);
|
||||
args.insert("commit_msg".to_string(), json!(message));
|
||||
args.insert(
|
||||
"use_individual_branch".to_string(),
|
||||
json!(workspace_git_repo.use_individual_branch.unwrap_or(false)),
|
||||
);
|
||||
|
||||
let tx: PushIsolationLevel<'_, R> =
|
||||
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq.clone());
|
||||
let tx: PushIsolationLevel<'_, R> = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
|
||||
|
||||
let (job_uuid, new_tx) = windmill_queue::push(
|
||||
db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::DeploymentCallback { path: workspace_git_repo.script_path.clone() },
|
||||
args,
|
||||
created_by,
|
||||
&email,
|
||||
SUPERADMIN_SYNC_EMAIL.to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
new_tx.commit().await?;
|
||||
|
||||
git_sync_job_uuids.push(job_uuid);
|
||||
}
|
||||
}
|
||||
let (job_uuid, new_tx) = windmill_queue::push(
|
||||
db,
|
||||
tx,
|
||||
&w_id,
|
||||
JobPayload::DeploymentCallback { path: workspace_git_repo.script_path.clone() },
|
||||
args,
|
||||
created_by,
|
||||
&email,
|
||||
SUPERADMIN_SYNC_EMAIL.to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
new_tx.commit().await?;
|
||||
Some(job_uuid)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// We're not persisting the default commit msg as it's pretty useless. We will persist the ones manually set by users
|
||||
if !skip_db_insert && (deployment_message.is_some() || git_sync_job_uuids.len() > 0) {
|
||||
let job_uuids = if git_sync_job_uuid.is_some() {
|
||||
vec![git_sync_job_uuid.unwrap()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
if deployment_message.is_some() || job_uuids.len() > 0 {
|
||||
// if the git sync job hasn't been triggered, and there is not custom deployment message, there's not point adding an entry to the table
|
||||
match obj.clone() {
|
||||
DeployedObject::Script { path, hash, .. } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id, path, hash.0, &git_sync_job_uuids, deployment_message,
|
||||
w_id, path, hash.0, &job_uuids, deployment_message,
|
||||
)
|
||||
},
|
||||
DeployedObject::Flow { path, .. } => {
|
||||
DeployedObject::Flow { path } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id, path) WHERE script_hash IS NULL AND app_version IS NULL DO UPDATE SET callback_job_ids = $3, deployment_msg = $4",
|
||||
w_id, path, &git_sync_job_uuids, deployment_message,
|
||||
w_id, path, &job_uuids, deployment_message,
|
||||
)
|
||||
}
|
||||
DeployedObject::App { path, version, .. } => {
|
||||
DeployedObject::App { path, version } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
w_id, path, version, &git_sync_job_uuids, deployment_message,
|
||||
w_id, path, version, &job_uuids, deployment_message,
|
||||
)
|
||||
}
|
||||
}.execute(db)
|
||||
|
||||
@@ -39,5 +39,4 @@ itertools.workspace = true
|
||||
async-recursion.workspace = true
|
||||
bigdecimal.workspace = true
|
||||
axum.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
regex.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
@@ -25,7 +25,6 @@ use bigdecimal::ToPrimitive;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use itertools::Itertools;
|
||||
use prometheus::IntCounter;
|
||||
use regex::Regex;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, CONTENT_TYPE},
|
||||
Client, StatusCode,
|
||||
@@ -1006,7 +1005,6 @@ pub async fn handle_maybe_scheduled_job<'c, R: rsmq_async::RsmqConnection + Clon
|
||||
retry: schedule.retry,
|
||||
summary: schedule.summary,
|
||||
no_flow_overlap: schedule.no_flow_overlap,
|
||||
tag: schedule.tag,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -2293,10 +2291,6 @@ impl From<HashMap<String, Box<JsonRawValue>>> for PushArgs<HashMap<String, Box<J
|
||||
// }
|
||||
// }
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref RE_ARG_TAG: Regex = Regex::new(r#"\$args\[(\w+)\]"#).unwrap();
|
||||
}
|
||||
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
_db: &Pool<Postgres>,
|
||||
@@ -2502,35 +2496,23 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
dedicated_worker,
|
||||
None,
|
||||
),
|
||||
JobPayload::Dependencies { hash, language, path, dedicated_worker } => (
|
||||
Some(hash.0),
|
||||
Some(path),
|
||||
None,
|
||||
JobKind::Dependencies,
|
||||
None,
|
||||
None,
|
||||
Some(language),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
dedicated_worker,
|
||||
None,
|
||||
),
|
||||
JobPayload::RawScriptDependencies { script_path, content, language } => (
|
||||
None,
|
||||
Some(script_path),
|
||||
Some((content, None)),
|
||||
JobKind::Dependencies,
|
||||
None,
|
||||
None,
|
||||
Some(language),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
JobPayload::FlowDependencies { path, dedicated_worker } => {
|
||||
JobPayload::Dependencies { hash, language, path, dedicated_worker, deployment_message } => {
|
||||
(
|
||||
Some(hash.0),
|
||||
Some(path),
|
||||
Some((deployment_message.unwrap_or_else(String::new), None)),
|
||||
JobKind::Dependencies,
|
||||
None,
|
||||
None,
|
||||
Some(language),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
dedicated_worker,
|
||||
None,
|
||||
)
|
||||
}
|
||||
JobPayload::FlowDependencies { path, dedicated_worker, deployment_message } => {
|
||||
let value_json = fetch_scalar_isolated!(
|
||||
sqlx::query_scalar!(
|
||||
"SELECT value FROM flow WHERE path = $1 AND workspace_id = $2",
|
||||
@@ -2548,7 +2530,7 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
(
|
||||
None,
|
||||
Some(path),
|
||||
None,
|
||||
Some((deployment_message.unwrap_or_else(String::new), None)),
|
||||
JobKind::FlowDependencies,
|
||||
Some(value.clone()),
|
||||
Some(FlowStatus::new(&value)), // this is a new flow being pushed, flow_status is set to flow_value
|
||||
@@ -2560,10 +2542,10 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
None,
|
||||
)
|
||||
}
|
||||
JobPayload::AppDependencies { path, version } => (
|
||||
JobPayload::AppDependencies { path, version, deployment_message } => (
|
||||
Some(version),
|
||||
Some(path),
|
||||
None,
|
||||
Some((deployment_message.unwrap_or_else(String::new), None)),
|
||||
JobKind::AppDependencies,
|
||||
None,
|
||||
None,
|
||||
@@ -2636,7 +2618,6 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
concurrency_time_window_s,
|
||||
cache_ttl,
|
||||
priority,
|
||||
tag_override,
|
||||
} => {
|
||||
let mut input_transforms = HashMap::<String, InputTransform>::new();
|
||||
for (arg_name, arg_value) in args {
|
||||
@@ -2649,7 +2630,6 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
input_transforms: input_transforms,
|
||||
path: path.clone(),
|
||||
hash: Some(hash),
|
||||
tag_override: tag_override,
|
||||
},
|
||||
stop_after_if: None,
|
||||
summary: None,
|
||||
@@ -2891,32 +2871,13 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
"deno".to_string()
|
||||
}
|
||||
};
|
||||
let interpolated_tag = tag.map(|x| {
|
||||
let workspaced = x.as_str().replace("$workspace", workspace_id).to_string();
|
||||
if RE_ARG_TAG.is_match(&workspaced) {
|
||||
let mut interpolated = workspaced.clone();
|
||||
for cap in RE_ARG_TAG.captures_iter(&workspaced) {
|
||||
let arg_name = cap.get(1).unwrap().as_str();
|
||||
let value = serde_json::to_value(&args).unwrap_or_default();
|
||||
let arg_value = value
|
||||
.get(arg_name)
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or_default();
|
||||
interpolated =
|
||||
interpolated.replace(format!("$args[{}]", arg_name).as_str(), arg_value);
|
||||
}
|
||||
interpolated
|
||||
} else {
|
||||
workspaced
|
||||
}
|
||||
});
|
||||
|
||||
interpolated_tag.unwrap_or_else(|| {
|
||||
language
|
||||
.as_ref()
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_else(default)
|
||||
})
|
||||
tag.map(|x| x.as_str().replace("$workspace", workspace_id).to_string())
|
||||
.unwrap_or_else(|| {
|
||||
language
|
||||
.as_ref()
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_else(default)
|
||||
})
|
||||
};
|
||||
|
||||
let mut tx = match tx {
|
||||
|
||||
@@ -130,7 +130,6 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
concurrency_time_window_s: concurrency_time_window_s,
|
||||
cache_ttl: cache_ttl,
|
||||
priority: priority,
|
||||
tag_override: schedule.tag.clone(),
|
||||
},
|
||||
Some("flow".to_string()),
|
||||
timeout,
|
||||
@@ -147,7 +146,7 @@ pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
language,
|
||||
priority,
|
||||
},
|
||||
schedule.tag.or(tag),
|
||||
tag,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:openidconnect", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"]
|
||||
benchmark = ["windmill-queue/benchmark"]
|
||||
flamegraph = []
|
||||
|
||||
@@ -78,7 +78,6 @@ reqwest.workspace = true
|
||||
hex.workspace = true
|
||||
tiberius = { workspace = true, optional = true }
|
||||
tokio-util = { workspace = true, optional = true }
|
||||
openidconnect = { workspace = true, optional = true}
|
||||
|
||||
[build-dependencies]
|
||||
deno_fetch.workspace = true
|
||||
|
||||
@@ -7,8 +7,7 @@ const bo = await Bun.build({
|
||||
|
||||
const fs = require("fs/promises");
|
||||
|
||||
const captureVersion =
|
||||
/^((?:\@[^\/\@]+\/[^\/\@]+)|(?:[^\/\@]+))(?:\@([^\/]+))?.*$/;
|
||||
const captureVersion = /(^\@?[^\@]+)(?:\@(.+))?/;
|
||||
|
||||
if (!bo.success) {
|
||||
bo.logs.forEach((l) => console.log(l));
|
||||
@@ -23,12 +22,6 @@ if (!bo.success) {
|
||||
const dependencies: Record<string, string[]> = {};
|
||||
for (const i of imports) {
|
||||
let [_, name, version] = i.path.match(captureVersion) ?? [];
|
||||
if (name == undefined) {
|
||||
throw Error("Unrecognized import: " + i.path);
|
||||
}
|
||||
if (name.startsWith("node:")) {
|
||||
continue;
|
||||
}
|
||||
let splitted = name.split("/");
|
||||
if (splitted.length > 2) {
|
||||
name = splitted.slice(0, 2).join("/");
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name: "{LANG} run script"
|
||||
name: "bun run script"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "{LANG}"
|
||||
hostname: "bun"
|
||||
log_level: ERROR
|
||||
|
||||
disable_rl: true
|
||||
|
||||
mount_proc: true
|
||||
|
||||
cwd: "/tmp/{LANG}"
|
||||
cwd: "/tmp/bun"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
@@ -64,14 +64,14 @@ mount {
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/package.json"
|
||||
dst: "/tmp/{LANG}/package.json"
|
||||
dst: "/tmp/bun/package.json"
|
||||
is_bind: true
|
||||
mandatory: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/bun.lockb"
|
||||
dst: "/tmp/{LANG}/bun.lockb"
|
||||
dst: "/tmp/bun/bun.lockb"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
@@ -79,14 +79,7 @@ mount {
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/wrapper.ts"
|
||||
dst: "/tmp/{LANG}/wrapper.ts"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/wrapper.mjs"
|
||||
dst: "/tmp/{LANG}/wrapper.mjs"
|
||||
dst: "/tmp/bun/wrapper.ts"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
@@ -94,22 +87,14 @@ mount {
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/loader.bun.ts"
|
||||
dst: "/tmp/{LANG}/loader.bun.ts"
|
||||
dst: "/tmp/bun/loader.bun.ts"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/node_modules"
|
||||
dst: "/tmp/{LANG}/node_modules"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/main.ts"
|
||||
dst: "/tmp/{LANG}/main.ts"
|
||||
dst: "/tmp/bun/main.ts"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
@@ -117,13 +102,13 @@ mount {
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/args.json"
|
||||
dst: "/tmp/{LANG}/args.json"
|
||||
dst: "/tmp/bun/args.json"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/result.json"
|
||||
dst: "/tmp/{LANG}/result.json"
|
||||
dst: "/tmp/bun/result.json"
|
||||
rw: true
|
||||
is_bind: true
|
||||
}
|
||||
@@ -158,6 +143,6 @@ mount {
|
||||
|
||||
{SHARED_MOUNT}
|
||||
|
||||
envar: "HOME=/tmp/{LANG}"
|
||||
envar: "HOME=/tmp/bun"
|
||||
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ use crate::common::build_envs_map;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
create_args_and_out_file, get_reserved_variables, handle_child, parse_npm_config,
|
||||
read_result, set_logs, start_child_process, write_file, write_file_binary,
|
||||
create_args_and_out_file, get_reserved_variables, handle_child, read_result, set_logs,
|
||||
start_child_process, write_file, write_file_binary,
|
||||
},
|
||||
AuthedClientBackgroundTask, BUNFIG_INSTALL_SCOPES, BUN_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL,
|
||||
DISABLE_NUSER, HOME_ENV, NODE_PATH, NPM_CONFIG_REGISTRY, NSJAIL_PATH, PATH_ENV, TZ_ENV,
|
||||
AuthedClientBackgroundTask, BUN_CACHE_DIR, BUN_PATH, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV,
|
||||
NPM_CONFIG_REGISTRY, NSJAIL_PATH, PATH_ENV, TZ_ENV,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
@@ -49,7 +49,6 @@ pub const EMPTY_FILE: &str = "<empty>";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TRUSTED_DEP: Regex = Regex::new(r"//\s?trustedDependencies:(.*)\n").unwrap();
|
||||
|
||||
}
|
||||
|
||||
pub async fn gen_lockfile(
|
||||
@@ -85,7 +84,37 @@ pub async fn gen_lockfile(
|
||||
)
|
||||
.await?;
|
||||
|
||||
gen_bunfig(job_dir).await?;
|
||||
// if custom NPM registry is being used, write bunfig.toml at the root of the job dir
|
||||
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
|
||||
let (url, token_opt) = if s.contains(":_authToken=") {
|
||||
let split_url = s.split(":_authToken=").collect::<Vec<&str>>();
|
||||
let url = split_url
|
||||
.get(0)
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
let token = split_url
|
||||
.get(1)
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
(url, Some(token))
|
||||
} else {
|
||||
(s.to_owned(), None)
|
||||
};
|
||||
let registry_toml_string = if let Some(token) = token_opt {
|
||||
format!("{{ url = \"{url}\", token = \"{token}\" }}")
|
||||
} else {
|
||||
format!("\"{url}\"")
|
||||
};
|
||||
let bunfig_toml = format!(
|
||||
r#"
|
||||
[install]
|
||||
registry = {}
|
||||
"#,
|
||||
registry_toml_string
|
||||
);
|
||||
tracing::debug!("Writing following bunfig.toml: {bunfig_toml}");
|
||||
let _ = write_file(&job_dir, "bunfig.toml", &bunfig_toml).await?;
|
||||
}
|
||||
|
||||
let common_bun_proc_envs: HashMap<String, String> =
|
||||
get_common_bun_proc_envs(&base_internal_url).await;
|
||||
@@ -184,42 +213,6 @@ pub async fn gen_lockfile(
|
||||
}
|
||||
}
|
||||
|
||||
async fn gen_bunfig(job_dir: &str) -> Result<()> {
|
||||
let registry = NPM_CONFIG_REGISTRY.read().await.clone();
|
||||
let bunfig_install_scopes = BUNFIG_INSTALL_SCOPES.read().await.clone();
|
||||
Ok(if registry.is_some() || bunfig_install_scopes.is_some() {
|
||||
let (url, token_opt) = if let Some(ref s) = registry {
|
||||
let url = s.trim();
|
||||
if url.is_empty() {
|
||||
("https://registry.npmjs.org".to_string(), None)
|
||||
} else {
|
||||
parse_npm_config(s)
|
||||
}
|
||||
} else {
|
||||
("https://registry.npmjs.org".to_string(), None)
|
||||
};
|
||||
let registry_toml_string = if let Some(token) = token_opt {
|
||||
format!("{{ url = \"{url}\", token = \"{token}\" }}")
|
||||
} else {
|
||||
format!("\"{url}\"")
|
||||
};
|
||||
let bunfig_toml = format!(
|
||||
r#"
|
||||
[install]
|
||||
registry = {}
|
||||
|
||||
{}
|
||||
"#,
|
||||
registry_toml_string,
|
||||
bunfig_install_scopes
|
||||
.map(|x| format!("[install.scopes]\n{x}"))
|
||||
.unwrap_or("".to_string())
|
||||
);
|
||||
tracing::debug!("Writing following bunfig.toml: {bunfig_toml}");
|
||||
let _ = write_file(&job_dir, "bunfig.toml", &bunfig_toml).await?;
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn install_lockfile(
|
||||
logs: &mut String,
|
||||
mem_peak: &mut i32,
|
||||
@@ -241,7 +234,6 @@ pub async fn install_lockfile(
|
||||
.stderr(Stdio::piped());
|
||||
let child_process = start_child_process(child_cmd, &*BUN_PATH).await?;
|
||||
|
||||
gen_bunfig(job_dir).await?;
|
||||
handle_child(
|
||||
job_id,
|
||||
db,
|
||||
@@ -298,18 +290,6 @@ pub async fn handle_bun_job(
|
||||
let common_bun_proc_envs: HashMap<String, String> =
|
||||
get_common_bun_proc_envs(&base_internal_url).await;
|
||||
|
||||
let nodejs_mode: bool = inner_content.starts_with("//nodejs");
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if nodejs_mode {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"Nodejs mode is an EE feature".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let has_custom_config_registry =
|
||||
NPM_CONFIG_REGISTRY.read().await.is_some() || BUNFIG_INSTALL_SCOPES.read().await.is_some();
|
||||
|
||||
if let Some(reqs) = requirements_o {
|
||||
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
|
||||
if splitted.len() != 2 {
|
||||
@@ -347,7 +327,7 @@ pub async fn handle_bun_job(
|
||||
common_bun_proc_envs.clone(),
|
||||
)
|
||||
.await?;
|
||||
if !has_trusted_deps && !has_custom_config_registry && !nodejs_mode {
|
||||
if !has_trusted_deps {
|
||||
remove_dir_all(format!("{}/node_modules", job_dir)).await?;
|
||||
}
|
||||
}
|
||||
@@ -355,7 +335,7 @@ pub async fn handle_bun_job(
|
||||
// TODO: remove once bun implement a reasonable set of trusted deps
|
||||
let trusted_deps = get_trusted_deps(inner_content);
|
||||
let empty_trusted_deps = trusted_deps.len() == 0;
|
||||
|
||||
let has_custom_config_registry = common_bun_proc_envs.contains_key("NPM_CONFIG_REGISTRY");
|
||||
// if !*DISABLE_NSJAIL || !empty_trusted_deps || has_custom_config_registry {
|
||||
logs.push_str("\n\n--- BUN INSTALL ---\n");
|
||||
set_logs(&logs, &job.id, &db).await;
|
||||
@@ -377,7 +357,7 @@ pub async fn handle_bun_job(
|
||||
.await?;
|
||||
// }
|
||||
|
||||
if empty_trusted_deps && !has_custom_config_registry && !nodejs_mode {
|
||||
if empty_trusted_deps && !has_custom_config_registry {
|
||||
let node_modules_path = format!("{}/node_modules", job_dir);
|
||||
let node_modules_exists = tokio::fs::metadata(&node_modules_path).await.is_ok();
|
||||
if node_modules_exists {
|
||||
@@ -386,11 +366,7 @@ pub async fn handle_bun_job(
|
||||
}
|
||||
}
|
||||
|
||||
if nodejs_mode {
|
||||
logs.push_str("\n\n--- NODE CODE EXECUTION ---\n");
|
||||
} else {
|
||||
logs.push_str("\n\n--- BUN CODE EXECUTION ---\n");
|
||||
}
|
||||
logs.push_str("\n\n--- BUN CODE EXECUTION ---\n");
|
||||
|
||||
let logs_f = async {
|
||||
set_logs(&logs, &job.id, &db).await;
|
||||
@@ -437,12 +413,7 @@ async function run() {{
|
||||
process.exit(0);
|
||||
}}
|
||||
run().catch(async (e) => {{
|
||||
let err = {{ message: e.message, name: e.name, stack: e.stack }};
|
||||
let step_id = process.env.WM_FLOW_STEP_ID;
|
||||
if (step_id) {{
|
||||
err["step_id"] = step_id;
|
||||
}}
|
||||
await fs.writeFile("result.json", JSON.stringify(err));
|
||||
await fs.writeFile("result.json", JSON.stringify({{ message: e.message, name: e.name, stack: e.stack }}));
|
||||
process.exit(1);
|
||||
}});
|
||||
"#,
|
||||
@@ -465,65 +436,27 @@ run().catch(async (e) => {{
|
||||
Ok(reserved_variables) as error::Result<HashMap<String, String>>
|
||||
};
|
||||
|
||||
let loader = RELATIVE_BUN_LOADER
|
||||
.replace("W_ID", &job.workspace_id)
|
||||
.replace("BASE_INTERNAL_URL", base_internal_url)
|
||||
.replace("TOKEN", &client.get_token().await)
|
||||
.replace("CURRENT_PATH", job.script_path());
|
||||
let write_loader_f = async move {
|
||||
if nodejs_mode {
|
||||
write_file(
|
||||
&job_dir,
|
||||
"node_builder.ts",
|
||||
&format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
import {{ readdir }} from "node:fs/promises";
|
||||
|
||||
let fileNames = []
|
||||
try {{
|
||||
fileNames = await readdir("{job_dir}/node_modules")
|
||||
}} catch (e) {{
|
||||
|
||||
}}
|
||||
|
||||
const bo = await Bun.build({{
|
||||
entrypoints: ["{job_dir}/wrapper.ts"],
|
||||
outdir: "./",
|
||||
target: "node",
|
||||
plugins: [p],
|
||||
external: fileNames,
|
||||
}});
|
||||
|
||||
if (!bo.success) {{
|
||||
bo.logs.forEach((l) => console.log(l));
|
||||
process.exit(1);
|
||||
}}
|
||||
"#,
|
||||
loader
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(()) as error::Result<()>
|
||||
} else {
|
||||
write_file(
|
||||
&job_dir,
|
||||
"loader.bun.ts",
|
||||
&format!(
|
||||
r#"
|
||||
let write_loader_f = async {
|
||||
write_file(
|
||||
&job_dir,
|
||||
"loader.bun.ts",
|
||||
&format!(
|
||||
r#"
|
||||
import {{ plugin }} from "bun";
|
||||
|
||||
{}
|
||||
|
||||
plugin(p)
|
||||
"#,
|
||||
loader
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(()) as error::Result<()>
|
||||
}
|
||||
RELATIVE_BUN_LOADER
|
||||
.replace("W_ID", &job.workspace_id)
|
||||
.replace("BASE_INTERNAL_URL", base_internal_url)
|
||||
.replace("TOKEN", &client.get_token().await)
|
||||
.replace("CURRENT_PATH", job.script_path())
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(()) as error::Result<()>
|
||||
};
|
||||
|
||||
let (reserved_variables, _, _, _) = tokio::try_join!(
|
||||
@@ -533,47 +466,12 @@ plugin(p)
|
||||
write_loader_f
|
||||
)?;
|
||||
|
||||
if nodejs_mode {
|
||||
let mut child = Command::new(&*BUN_PATH);
|
||||
child
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(common_bun_proc_envs.clone())
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.args(vec!["run", "node_builder.ts"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let child_process = start_child_process(child, &*BUN_PATH).await?;
|
||||
handle_child(
|
||||
&job.id,
|
||||
db,
|
||||
logs,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
child_process,
|
||||
false,
|
||||
worker_name,
|
||||
&job.workspace_id,
|
||||
"bun build",
|
||||
job.timeout,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::rename(
|
||||
format!("{job_dir}/wrapper.js"),
|
||||
format!("{job_dir}/wrapper.mjs"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| error::Error::InternalErr(format!("Could not move wrapper to mjs: {e}")))?;
|
||||
}
|
||||
|
||||
//do not cache local dependencies
|
||||
let child = if !*DISABLE_NSJAIL {
|
||||
let _ = write_file(
|
||||
job_dir,
|
||||
"run.config.proto",
|
||||
&NSJAIL_CONFIG_RUN_BUN_CONTENT
|
||||
.replace("{LANG}", if nodejs_mode { "nodejs" } else { "bun" })
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{CACHE_DIR}", BUN_CACHE_DIR)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
@@ -582,16 +480,14 @@ plugin(p)
|
||||
.await?;
|
||||
|
||||
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
|
||||
let args = if nodejs_mode {
|
||||
vec![
|
||||
"--config",
|
||||
"run.config.proto",
|
||||
"--",
|
||||
&NODE_PATH,
|
||||
"/tmp/nodejs/wrapper.mjs",
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
nsjail_cmd
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(common_bun_proc_envs)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.args(vec![
|
||||
"--config",
|
||||
"run.config.proto",
|
||||
"--",
|
||||
@@ -602,57 +498,31 @@ plugin(p)
|
||||
"-r",
|
||||
"/tmp/bun/loader.bun.ts",
|
||||
"/tmp/bun/wrapper.ts",
|
||||
]
|
||||
};
|
||||
nsjail_cmd
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
|
||||
} else {
|
||||
let script_path = format!("{job_dir}/wrapper.ts");
|
||||
let args = vec![
|
||||
"run",
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"./loader.bun.ts",
|
||||
&script_path,
|
||||
];
|
||||
let mut bun_cmd = Command::new(&*BUN_PATH);
|
||||
bun_cmd
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(common_bun_proc_envs)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
|
||||
} else {
|
||||
let cmd = if nodejs_mode {
|
||||
let script_path = format!("{job_dir}/wrapper.mjs");
|
||||
|
||||
let mut bun_cmd = Command::new(&*NODE_PATH);
|
||||
bun_cmd
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(common_bun_proc_envs)
|
||||
.args(vec![&script_path])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
bun_cmd
|
||||
} else {
|
||||
let script_path = format!("{job_dir}/wrapper.ts");
|
||||
|
||||
let mut bun_cmd = Command::new(&*BUN_PATH);
|
||||
bun_cmd
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(common_bun_proc_envs)
|
||||
.args(vec![
|
||||
"run",
|
||||
"-i",
|
||||
"--prefer-offline",
|
||||
"-r",
|
||||
"./loader.bun.ts",
|
||||
&script_path,
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
bun_cmd
|
||||
};
|
||||
start_child_process(cmd, &*BUN_PATH).await?
|
||||
start_child_process(bun_cmd, &*BUN_PATH).await?
|
||||
};
|
||||
|
||||
handle_child(
|
||||
@@ -733,8 +603,6 @@ pub async fn start_worker(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let context_envs = build_envs_map(context.to_vec()).await;
|
||||
|
||||
@@ -197,24 +197,6 @@ pub async fn transform_json_as_values<'a>(
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn parse_npm_config(s: &str) -> (String, Option<String>) {
|
||||
let (url, token_opt) = if s.contains(":_authToken=") {
|
||||
let split_url = s.split(":_authToken=").collect::<Vec<&str>>();
|
||||
let url = split_url
|
||||
.get(0)
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
let token = split_url
|
||||
.get(1)
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or("".to_string());
|
||||
(url, Some(token))
|
||||
} else {
|
||||
(s.to_owned(), None)
|
||||
};
|
||||
return (url, token_opt);
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
pub async fn transform_json_value(
|
||||
name: &str,
|
||||
@@ -274,8 +256,6 @@ pub async fn transform_json_value(
|
||||
flow_path,
|
||||
job.schedule_path.clone(),
|
||||
job.flow_step_id.clone(),
|
||||
job.root_job.clone().map(|x| x.to_string()),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -385,8 +365,6 @@ pub async fn get_reserved_variables(
|
||||
flow_path,
|
||||
job.schedule_path.clone(),
|
||||
job.flow_step_id.clone(),
|
||||
job.root_job.clone().map(|x| x.to_string()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.to_vec();
|
||||
@@ -667,7 +645,7 @@ pub async fn handle_child(
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
append_with_limit(&mut joined, &line, &mut log_remaining);
|
||||
append_with_limit(&mut joined, &line, &mut log_remaining, child_name == "powershell run" || child_name == "bash run");
|
||||
if log_remaining == 0 {
|
||||
tracing::info!(%job_id, "Too many logs lines for job {job_id}");
|
||||
let _ = set_too_many_logs.send(true);
|
||||
@@ -852,16 +830,15 @@ pub fn lines_to_stream<R: tokio::io::AsyncBufRead + Unpin>(
|
||||
})
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE_00: Regex = Regex::new('\u{00}'.to_string().as_str()).unwrap();
|
||||
}
|
||||
// as a detail, `BufReader::lines()` removes \n and \r\n from the strings it yields,
|
||||
// so this pushes \n to thd destination string in each call
|
||||
fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize) {
|
||||
fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize, remove_x00: bool) {
|
||||
let src_str;
|
||||
let src = {
|
||||
src_str = RE_00.replace_all(src, "");
|
||||
src_str.as_ref()
|
||||
let src = if remove_x00 {
|
||||
src_str = src.replace('\u{00}', "");
|
||||
src_str.as_str()
|
||||
} else {
|
||||
src
|
||||
};
|
||||
if !*CLOUD_HOSTED {
|
||||
dst.push('\n');
|
||||
|
||||
@@ -48,7 +48,7 @@ pub async fn handle_dedicated_process(
|
||||
job_dir: &str,
|
||||
context_envs: HashMap<String, String>,
|
||||
envs: HashMap<String, String>,
|
||||
reserved_variables: [variables::ContextualVariable; 17],
|
||||
reserved_variables: [variables::ContextualVariable; 15],
|
||||
common_bun_proc_envs: HashMap<String, String>,
|
||||
args: Vec<&str>,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
|
||||
@@ -7,8 +7,8 @@ use windmill_queue::CanceledBy;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
create_args_and_out_file, get_reserved_variables, handle_child, parse_npm_config,
|
||||
read_result, set_logs, start_child_process, write_file,
|
||||
create_args_and_out_file, get_reserved_variables, handle_child, read_result, set_logs,
|
||||
start_child_process, write_file,
|
||||
},
|
||||
AuthedClientBackgroundTask, DENO_CACHE_DIR, DENO_PATH, DISABLE_NSJAIL, HOME_ENV,
|
||||
NPM_CONFIG_REGISTRY, PATH_ENV, TZ_ENV,
|
||||
@@ -70,8 +70,7 @@ async fn get_common_deno_proc_envs(
|
||||
]);
|
||||
|
||||
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
|
||||
let (url, _token_opt) = parse_npm_config(s);
|
||||
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url);
|
||||
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
|
||||
}
|
||||
if DENO_CERT.len() > 0 {
|
||||
deno_envs.insert(String::from("DENO_CERT"), DENO_CERT.clone());
|
||||
@@ -109,8 +108,7 @@ pub async fn generate_deno_lock(
|
||||
|
||||
let mut deno_envs = HashMap::new();
|
||||
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
|
||||
let (url, _token_opt) = parse_npm_config(s);
|
||||
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), url);
|
||||
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
|
||||
}
|
||||
let mut child_cmd = Command::new(DENO_PATH.as_str());
|
||||
child_cmd
|
||||
@@ -219,12 +217,7 @@ async function run() {{
|
||||
Deno.exit(0);
|
||||
}}
|
||||
run().catch(async (e) => {{
|
||||
let err = {{ message: e.message, name: e.name, stack: e.stack }};
|
||||
let step_id = Deno.env.get("WM_FLOW_STEP_ID");
|
||||
if (step_id) {{
|
||||
err["step_id"] = step_id;
|
||||
}}
|
||||
await Deno.writeTextFile("result.json", JSON.stringify(err));
|
||||
await Deno.writeTextFile("result.json", JSON.stringify({{ message: e.message, name: e.name, stack: e.stack }}));
|
||||
Deno.exit(1);
|
||||
}});
|
||||
"#,
|
||||
@@ -423,8 +416,6 @@ pub async fn start_worker(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let context_envs = build_envs_map(context.to_vec()).await;
|
||||
|
||||
@@ -16,7 +16,6 @@ use windmill_common::{
|
||||
error::{self, Error},
|
||||
jobs::QueuedJob,
|
||||
utils::calculate_hash,
|
||||
variables::get_secret_value_as_admin,
|
||||
worker::WORKER_CONFIG,
|
||||
DB,
|
||||
};
|
||||
@@ -108,10 +107,6 @@ pub async fn pip_compile(
|
||||
} else {
|
||||
requirements.to_string()
|
||||
};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
let requirements = replace_pip_secret(db, w_id, &requirements, worker_name, job_id).await?;
|
||||
|
||||
let req_hash = format!("py-{}", calculate_hash(&requirements));
|
||||
if let Some(cached) = sqlx::query_scalar!(
|
||||
"SELECT lockfile FROM pip_resolution_cache WHERE hash = $1",
|
||||
@@ -280,12 +275,7 @@ except BaseException 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 = {{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}
|
||||
import os
|
||||
flow_node_id = os.environ.get('WM_FLOW_STEP_ID')
|
||||
if flow_node_id:
|
||||
err['step_id'] = flow_node_id
|
||||
err_json = json.dumps(err, separators=(',', ':'), default=str).replace('\n', '')
|
||||
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)
|
||||
"#,
|
||||
@@ -528,37 +518,6 @@ if args["{name}"] is None:
|
||||
))
|
||||
}
|
||||
|
||||
async fn replace_pip_secret(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
req: &str,
|
||||
worker_name: &str,
|
||||
job_id: &Uuid,
|
||||
) -> error::Result<String> {
|
||||
if PIP_SECRET_VARIABLE.is_match(req) {
|
||||
let capture = PIP_SECRET_VARIABLE.captures(req);
|
||||
let variable = capture.unwrap().get(1).unwrap().as_str();
|
||||
if !variable.contains("/PIP_SECRET_") {
|
||||
return Err(error::Error::InternalErr(format!(
|
||||
"invalid secret variable in pip requirements, (last part of path ma): {}",
|
||||
req
|
||||
)));
|
||||
}
|
||||
let secret = get_secret_value_as_admin(db, w_id, variable).await?;
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job_id,
|
||||
workspace_id = %w_id,
|
||||
"found secret variable in pip requirements: {}",
|
||||
req
|
||||
);
|
||||
let req = PIP_SECRET_VARIABLE.replace(req, secret.as_str());
|
||||
Ok(req.to_string())
|
||||
} else {
|
||||
Ok(req.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_python_deps(
|
||||
job_dir: &str,
|
||||
requirements_o: Option<String>,
|
||||
@@ -638,10 +597,6 @@ async fn handle_python_deps(
|
||||
Ok(additional_python_paths)
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PIP_SECRET_VARIABLE: Regex = Regex::new(r"\$\{PIP_SECRET:([^s\}]+)\}").unwrap();
|
||||
}
|
||||
|
||||
pub async fn handle_python_reqs(
|
||||
requirements: Vec<&str>,
|
||||
job_id: &Uuid,
|
||||
@@ -750,7 +705,7 @@ pub async fn handle_python_reqs(
|
||||
.stderr(Stdio::piped());
|
||||
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
|
||||
} else {
|
||||
let fssafe_req = NON_ALPHANUM_CHAR.replace_all(&req, "_").to_string();
|
||||
let fssafe_req = NON_ALPHANUM_CHAR.replace_all(req, "_").to_string();
|
||||
let req = format!("'{}'", req);
|
||||
let mut command_args = vec![
|
||||
PYTHON_PATH.as_str(),
|
||||
@@ -888,8 +843,6 @@ pub async fn start_worker(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.to_vec();
|
||||
@@ -1007,8 +960,6 @@ for line in sys.stdin:
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ pub async fn create_token_for_owner_in_bg(
|
||||
let w_id = job.workspace_id.clone();
|
||||
let owner = job.permissioned_as.clone();
|
||||
let email = job.email.clone();
|
||||
let job_id = job.id.clone();
|
||||
tokio::spawn(async move {
|
||||
let token = create_token_for_owner(
|
||||
&db.clone(),
|
||||
@@ -122,7 +121,6 @@ pub async fn create_token_for_owner_in_bg(
|
||||
"ephemeral-script",
|
||||
*SCRIPT_TOKEN_EXPIRY,
|
||||
&email,
|
||||
&job_id,
|
||||
)
|
||||
.await
|
||||
.expect("could not create job token");
|
||||
@@ -140,7 +138,6 @@ pub async fn create_token_for_owner(
|
||||
label: &str,
|
||||
expires_in: u64,
|
||||
email: &str,
|
||||
job_id: &Uuid,
|
||||
) -> error::Result<String> {
|
||||
// TODO: Bad implementation. We should not have access to this DB here.
|
||||
if let Some(token) = JOB_TOKEN.as_ref() {
|
||||
@@ -159,16 +156,15 @@ pub async fn create_token_for_owner(
|
||||
|
||||
sqlx::query_scalar!(
|
||||
"INSERT INTO token
|
||||
(workspace_id, token, owner, label, expiration, super_admin, email, job)
|
||||
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7, $8)",
|
||||
(workspace_id, token, owner, label, expiration, super_admin, email)
|
||||
VALUES ($1, $2, $3, $4, now() + ($5 || ' seconds')::interval, $6, $7)",
|
||||
&w_id,
|
||||
token,
|
||||
owner,
|
||||
label,
|
||||
expires_in.to_string(),
|
||||
is_super_admin,
|
||||
email,
|
||||
job_id
|
||||
email
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
@@ -242,7 +238,6 @@ lazy_static::lazy_static! {
|
||||
pub static ref HTTPS_PROXY: Option<String> = std::env::var("https_proxy").ok().or(std::env::var("HTTPS_PROXY").ok());
|
||||
pub static ref DENO_PATH: String = std::env::var("DENO_PATH").unwrap_or_else(|_| "/usr/bin/deno".to_string());
|
||||
pub static ref BUN_PATH: String = std::env::var("BUN_PATH").unwrap_or_else(|_| "/usr/bin/bun".to_string());
|
||||
pub static ref NODE_PATH: String = std::env::var("NODE_PATH").unwrap_or_else(|_| "/usr/bin/node".to_string());
|
||||
pub static ref POWERSHELL_PATH: String = std::env::var("POWERSHELL_PATH").unwrap_or_else(|_| "/usr/bin/pwsh".to_string());
|
||||
pub static ref NSJAIL_PATH: String = std::env::var("NSJAIL_PATH").unwrap_or_else(|_| "nsjail".to_string());
|
||||
pub static ref PATH_ENV: String = std::env::var("PATH").unwrap_or_else(|_| String::new());
|
||||
@@ -253,8 +248,6 @@ lazy_static::lazy_static! {
|
||||
pub static ref NETRC: Option<String> = std::env::var("NETRC").ok();
|
||||
|
||||
pub static ref NPM_CONFIG_REGISTRY: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
pub static ref BUNFIG_INSTALL_SCOPES: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
|
||||
pub static ref PIP_EXTRA_INDEX_URL: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
pub static ref JOB_DEFAULT_TIMEOUT: Arc<RwLock<Option<i32>>> = Arc::new(RwLock::new(None));
|
||||
|
||||
@@ -1190,8 +1183,8 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
if let Ok(flow) = value {
|
||||
let workers = spawn_dedicated_workers_for_flow(
|
||||
&flow.modules,
|
||||
&_wp.workspace_id,
|
||||
&_wp.path,
|
||||
&_wp.workspace_id,
|
||||
killpill_tx.clone(),
|
||||
&killpill_rx,
|
||||
db,
|
||||
@@ -1202,13 +1195,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
)
|
||||
.await;
|
||||
workers.into_iter().for_each(|(path, sender, handle)| {
|
||||
tracing::info!(
|
||||
"spawned dedicated worker for flow: {}",
|
||||
path.as_str()
|
||||
);
|
||||
if let Some(h) = handle {
|
||||
dedicated_handles.push(h);
|
||||
}
|
||||
dedicated_handles.push(handle);
|
||||
hm.insert(path, sender);
|
||||
});
|
||||
}
|
||||
@@ -1237,9 +1224,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(h) = handle {
|
||||
dedicated_handles.push(h);
|
||||
}
|
||||
dedicated_handles.push(handle);
|
||||
hm.insert(path, sender);
|
||||
} else {
|
||||
tracing::error!(
|
||||
@@ -1769,7 +1754,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
println!("worker {} exited", i_worker);
|
||||
}
|
||||
|
||||
type DedicatedWorker = (String, Sender<Arc<QueuedJob>>, Option<JoinHandle<()>>);
|
||||
type DedicatedWorker = (String, Sender<Arc<QueuedJob>>, JoinHandle<()>);
|
||||
|
||||
// spawn one dedicated worker per compatible steps of the flow, associating the node id to the dedicated worker channel send
|
||||
#[async_recursion]
|
||||
@@ -1786,37 +1771,24 @@ async fn spawn_dedicated_workers_for_flow(
|
||||
job_completed_tx: &JobCompletedSender,
|
||||
) -> Vec<DedicatedWorker> {
|
||||
let mut workers = vec![];
|
||||
let mut script_path_to_worker: HashMap<String, Sender<Arc<QueuedJob>>> = HashMap::new();
|
||||
for module in modules.iter() {
|
||||
match &module.value {
|
||||
FlowModuleValue::Script { path, hash, .. } => {
|
||||
let key = format!(
|
||||
"{}:{}",
|
||||
path,
|
||||
hash.clone()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
);
|
||||
if let Some(sender) = script_path_to_worker.get(&key) {
|
||||
workers.push((module.id.clone(), sender.clone(), None));
|
||||
} else {
|
||||
if let Some(dedi_w) = spawn_dedicated_worker(
|
||||
SpawnWorker::Script { path: path.to_string(), hash: hash.clone() },
|
||||
w_id,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
Some(module.id.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
script_path_to_worker.insert(key, dedi_w.1.clone());
|
||||
workers.push(dedi_w);
|
||||
}
|
||||
if let Some(dedi_w) = spawn_dedicated_worker(
|
||||
SpawnWorker::Script { path: path.to_string(), hash: hash.clone() },
|
||||
w_id,
|
||||
killpill_tx.clone(),
|
||||
killpill_rx,
|
||||
db,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
job_completed_tx,
|
||||
Some(module.id.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
workers.push(dedi_w);
|
||||
}
|
||||
}
|
||||
FlowModuleValue::ForloopFlow { modules, .. } => {
|
||||
@@ -2079,7 +2051,7 @@ async fn spawn_dedicated_worker(
|
||||
tracing::error!("error in dedicated worker: {:?}", e);
|
||||
};
|
||||
});
|
||||
return Some((node_id.unwrap_or(path2), dedicated_worker_tx, Some(handle)));
|
||||
return Some((node_id.unwrap_or(path2), dedicated_worker_tx, handle));
|
||||
// (Some(dedi_path), Some(dedicated_worker_tx), Some(handle))
|
||||
}
|
||||
}
|
||||
@@ -2521,7 +2493,7 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
&job.args,
|
||||
)
|
||||
.await;
|
||||
if job.is_flow_step && !matches!(job.job_kind, JobKind::Flow) {
|
||||
if job.is_flow_step {
|
||||
let flow_path = sqlx::query_scalar!(
|
||||
"SELECT script_path FROM queue WHERE id = $1",
|
||||
&job.parent_job.unwrap()
|
||||
@@ -3177,18 +3149,14 @@ async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clo
|
||||
token: &str,
|
||||
rsmq: Option<R>,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let raw_code = match job.raw_code {
|
||||
Some(ref code) => code.to_owned(),
|
||||
None => sqlx::query_scalar!(
|
||||
"SELECT content FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
&job.script_hash.unwrap_or(ScriptHash(0)).0,
|
||||
&job.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or_else(|| "No script found at this hash".to_string()),
|
||||
};
|
||||
|
||||
let raw_code = sqlx::query_scalar!(
|
||||
"SELECT content FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
&job.script_hash.unwrap_or(ScriptHash(0)).0,
|
||||
&job.workspace_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or_else(|| "No script found at this hash".to_string());
|
||||
let script_path = job.script_path();
|
||||
let content = capture_dependency_job(
|
||||
&job.id,
|
||||
@@ -3213,13 +3181,6 @@ async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clo
|
||||
.await;
|
||||
match content {
|
||||
Ok(content) => {
|
||||
if job.script_hash.is_none() {
|
||||
// it a one-off raw script dependency job, no need to update the db
|
||||
return Ok(to_raw_value_owned(
|
||||
json!({ "success": "Successful lock file generation", "lock": content }),
|
||||
));
|
||||
}
|
||||
|
||||
let hash = job.script_hash.unwrap_or(ScriptHash(0));
|
||||
let w_id = &job.workspace_id;
|
||||
sqlx::query!(
|
||||
@@ -3231,22 +3192,16 @@ async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clo
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
let (deployment_message, parent_path) =
|
||||
get_deployment_msg_and_parent_path_from_args(job.args.clone());
|
||||
let deployment_message = job.raw_code.as_ref().map(|x| x.clone());
|
||||
|
||||
if let Err(e) = handle_deployment_metadata(
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script {
|
||||
hash,
|
||||
path: script_path.to_string(),
|
||||
parent_path: parent_path.clone(),
|
||||
},
|
||||
DeployedObject::Script { hash, path: script_path.to_string() },
|
||||
deployment_message.clone(),
|
||||
rsmq.clone(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -3286,7 +3241,6 @@ async fn handle_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync + Clo
|
||||
w_id,
|
||||
script_path,
|
||||
deployment_message,
|
||||
parent_path,
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&job.permissioned_as,
|
||||
@@ -3323,7 +3277,6 @@ async fn trigger_python_dependents_to_recompute_dependencies<
|
||||
w_id: &str,
|
||||
script_path: &str,
|
||||
deployment_message: Option<String>,
|
||||
parent_path: Option<String>,
|
||||
email: &str,
|
||||
created_by: &str,
|
||||
permissioned_as: &str,
|
||||
@@ -3344,14 +3297,6 @@ async fn trigger_python_dependents_to_recompute_dependencies<
|
||||
PushIsolationLevel::IsolatedRoot(db.clone(), rsmq.clone());
|
||||
let r = get_latest_deployed_hash_for_path(db, w_id, s.as_str()).await;
|
||||
if let Ok(r) = r {
|
||||
let mut args: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
if let Some(ref dm) = deployment_message {
|
||||
args.insert("deployment_message".to_string(), json!(dm));
|
||||
}
|
||||
if let Some(ref p_path) = parent_path {
|
||||
args.insert("parent_path".to_string(), json!(p_path));
|
||||
}
|
||||
|
||||
let (job_uuid, new_tx) = windmill_queue::push(
|
||||
db,
|
||||
tx,
|
||||
@@ -3361,8 +3306,9 @@ async fn trigger_python_dependents_to_recompute_dependencies<
|
||||
hash: r.0,
|
||||
language: r.5,
|
||||
dedicated_worker: r.6,
|
||||
deployment_message: deployment_message.clone(),
|
||||
},
|
||||
args,
|
||||
PushArgs::empty(),
|
||||
&created_by,
|
||||
email,
|
||||
permissioned_as.to_string(),
|
||||
@@ -3462,18 +3408,14 @@ async fn handle_flow_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
let (deployment_message, parent_path) =
|
||||
get_deployment_msg_and_parent_path_from_args(job.args.clone());
|
||||
|
||||
if let Err(e) = handle_deployment_metadata(
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&db,
|
||||
&job.workspace_id,
|
||||
DeployedObject::Flow { path: job_path, parent_path },
|
||||
deployment_message,
|
||||
DeployedObject::Flow { path: job_path },
|
||||
job.raw_code.as_ref().map(|x| x.clone()),
|
||||
rsmq.clone(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -3483,31 +3425,6 @@ async fn handle_flow_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_deployment_msg_and_parent_path_from_args(
|
||||
args: Option<Json<HashMap<String, Box<RawValue>>>>,
|
||||
) -> (Option<String>, Option<String>) {
|
||||
let args_map = args.map(|json_hashmap| json_hashmap.0);
|
||||
let deployment_message = args_map
|
||||
.clone()
|
||||
.map(|hashmap| {
|
||||
hashmap
|
||||
.get("deployment_message")
|
||||
.map(|map_value| serde_json::from_str::<String>(map_value.get()).ok())
|
||||
.flatten()
|
||||
})
|
||||
.flatten();
|
||||
let parent_path = args_map
|
||||
.clone()
|
||||
.map(|hashmap| {
|
||||
hashmap
|
||||
.get("parent_path")
|
||||
.map(|map_value| serde_json::from_str::<String>(map_value.get()).ok())
|
||||
.flatten()
|
||||
})
|
||||
.flatten();
|
||||
(deployment_message, parent_path)
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn lock_modules(
|
||||
modules: Vec<FlowModule>,
|
||||
@@ -3871,18 +3788,14 @@ async fn handle_app_dependency_job<R: rsmq_async::RsmqConnection + Send + Sync +
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
let (deployment_message, parent_path) =
|
||||
get_deployment_msg_and_parent_path_from_args(job.args.clone());
|
||||
|
||||
if let Err(e) = handle_deployment_metadata(
|
||||
&job.email,
|
||||
&job.created_by,
|
||||
&db,
|
||||
&job.workspace_id,
|
||||
DeployedObject::App { path: job_path, version: id, parent_path },
|
||||
deployment_message,
|
||||
DeployedObject::App { path: job_path, version: id },
|
||||
job.raw_code.as_ref().map(|x| x.clone()),
|
||||
rsmq.clone(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -638,7 +638,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
if flow_job.cache_ttl.is_some() && success {
|
||||
if flow_job.cache_ttl.is_some() {
|
||||
let cached_res_path = {
|
||||
let args_hash =
|
||||
hash_args(db, client, w_id, job_id_for_status, &flow_job.args).await;
|
||||
@@ -2310,10 +2310,8 @@ async fn compute_next_flow_transform(
|
||||
NextStatus::NextStep,
|
||||
))
|
||||
}
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
|
||||
let payload =
|
||||
script_to_payload(script_hash, script_path, db, flow_job, module, tag_override)
|
||||
.await?;
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, .. } => {
|
||||
let payload = script_to_payload(script_hash, script_path, db, flow_job, module).await?;
|
||||
Ok(NextFlowTransform::Continue(
|
||||
ContinuePayload::SingleJob(payload),
|
||||
NextStatus::NextStep,
|
||||
@@ -2811,8 +2809,8 @@ async fn payload_from_simple_module(
|
||||
let delete_after_use = module.delete_after_use.unwrap_or(false);
|
||||
Ok(match value {
|
||||
FlowModuleValue::Flow { path, .. } => flow_to_payload(path, &delete_after_use),
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
|
||||
script_to_payload(script_hash, script_path, db, flow_job, module, tag_override).await?
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, .. } => {
|
||||
script_to_payload(script_hash, script_path, db, flow_job, module).await?
|
||||
}
|
||||
FlowModuleValue::RawScript {
|
||||
path,
|
||||
@@ -2877,9 +2875,7 @@ async fn script_to_payload(
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
flow_job: &QueuedJob,
|
||||
module: &FlowModule,
|
||||
tag_override: &Option<String>,
|
||||
) -> Result<JobPayloadWithTag, Error> {
|
||||
tracing::warn!("Script tag override: {:?}", tag_override);
|
||||
let (payload, tag, delete_after_use, script_timeout) = if script_hash.is_none() {
|
||||
script_path_to_payload(script_path, &db, &flow_job.workspace_id).await?
|
||||
} else {
|
||||
@@ -2907,7 +2903,7 @@ async fn script_to_payload(
|
||||
dedicated_worker,
|
||||
priority,
|
||||
},
|
||||
tag_override.to_owned().or(tag),
|
||||
tag,
|
||||
delete_after_use,
|
||||
script_timeout,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.246.10";
|
||||
export const VERSION = "v1.236.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
26
cli/apps.ts
26
cli/apps.ts
@@ -2,6 +2,7 @@
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import {
|
||||
AppService,
|
||||
AppWithLastVersion,
|
||||
colors,
|
||||
Command,
|
||||
ListableApp,
|
||||
@@ -23,12 +24,12 @@ export interface AppFile {
|
||||
|
||||
export async function pushApp(
|
||||
workspace: string,
|
||||
filePath: string,
|
||||
app: AppFile | undefined,
|
||||
remotePath: string,
|
||||
app: AppFile | AppWithLastVersion | undefined,
|
||||
newApp: AppFile,
|
||||
message?: string
|
||||
): Promise<void> {
|
||||
const remotePath = removeType(filePath, "app");
|
||||
remotePath = removeType(remotePath, "app");
|
||||
// deleting old app if it exists in raw mode
|
||||
try {
|
||||
app = await AppService.getAppByPath({
|
||||
@@ -54,7 +55,6 @@ export async function pushApp(
|
||||
} else {
|
||||
console.log(colors.yellow.bold("Creating new app..."));
|
||||
|
||||
console.log(message);
|
||||
await AppService.createApp({
|
||||
workspace,
|
||||
requestBody: {
|
||||
@@ -102,16 +102,22 @@ async function push(opts: GlobalOptions, filePath: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
let app: AppWithLastVersion | undefined = undefined;
|
||||
try {
|
||||
app = await AppService.getAppByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
// app doesn't exist
|
||||
}
|
||||
|
||||
await pushApp(
|
||||
workspace.workspaceId,
|
||||
filePath,
|
||||
undefined,
|
||||
remotePath,
|
||||
app,
|
||||
parseFromFile(filePath)
|
||||
);
|
||||
=======
|
||||
await pushApp(workspace.workspaceId, filePath, parseFromFile(filePath));
|
||||
>>>>>>> Stashed changes
|
||||
console.log(colors.bold.underline.green("App pushed"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
export interface SchemaProperty {
|
||||
type: string | undefined
|
||||
description?: string
|
||||
pattern?: string
|
||||
default?: any
|
||||
enum?: string[]
|
||||
contentEncoding?: 'base64' | 'binary'
|
||||
format?: string
|
||||
items?: {
|
||||
type?: 'string' | 'number' | 'bytes' | 'object'
|
||||
contentEncoding?: 'base64'
|
||||
enum?: string[]
|
||||
}
|
||||
min?: number
|
||||
max?: number
|
||||
currency?: string
|
||||
currencyLocale?: string
|
||||
multiselect?: boolean
|
||||
customErrorMessage?: string
|
||||
properties?: { [name: string]: SchemaProperty }
|
||||
required?: string[]
|
||||
showExpr?: string
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { SchemaProperty } from "./common.ts";
|
||||
|
||||
export interface FlowDefinition {
|
||||
summary: string,
|
||||
description: string,
|
||||
value: {
|
||||
modules: any[]
|
||||
},
|
||||
schema: {
|
||||
$schema: string,
|
||||
type: string,
|
||||
order: string[],
|
||||
properties: { [name: string]: SchemaProperty},
|
||||
required: string[]
|
||||
}
|
||||
ws_error_handler_muted: false
|
||||
}
|
||||
|
||||
export function defaultFlowDefinition(): FlowDefinition {
|
||||
return {
|
||||
summary: '',
|
||||
description: '',
|
||||
value: {
|
||||
modules: []
|
||||
},
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
type: 'object',
|
||||
order: [],
|
||||
properties: {},
|
||||
required: []
|
||||
},
|
||||
ws_error_handler_muted: false,
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { SchemaProperty } from "./common.ts";
|
||||
|
||||
export interface ScriptMetadata {
|
||||
summary: string;
|
||||
description: string;
|
||||
lock: string | string[];
|
||||
is_template: boolean;
|
||||
kind: string;
|
||||
schema: {
|
||||
$schema: string;
|
||||
type: string;
|
||||
properties: { [name: string]: SchemaProperty };
|
||||
required: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultScriptMetadata(): ScriptMetadata {
|
||||
return {
|
||||
summary: "",
|
||||
description: "",
|
||||
lock: "",
|
||||
is_template: false,
|
||||
kind: "script",
|
||||
schema: {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const scriptBootstrapCode = {
|
||||
python3: `def main():
|
||||
return "Hello world"
|
||||
`,
|
||||
|
||||
nativets: `export async function main() {
|
||||
return "Hello world";
|
||||
}
|
||||
`,
|
||||
|
||||
bun: `export async function main() {
|
||||
return "Hello world";
|
||||
}
|
||||
`,
|
||||
|
||||
deno: `export async function main() {
|
||||
return "Hello world";
|
||||
}
|
||||
`,
|
||||
|
||||
go: `package inner
|
||||
func main() (interface{}, error) {
|
||||
return "Hello world", nil
|
||||
}
|
||||
`,
|
||||
|
||||
mysql: `SELECT 'Hello world' AS message
|
||||
`,
|
||||
|
||||
bigquery: `SELECT 'Hello world' AS message
|
||||
`,
|
||||
|
||||
snowflake: `SELECT 'Hello world' AS message
|
||||
`,
|
||||
|
||||
mssql: `SELECT 'Hello world' AS message
|
||||
`,
|
||||
|
||||
graphql: `query() {
|
||||
demo() {}
|
||||
}`,
|
||||
|
||||
postgresql: `SELECT 'Hello world' AS message
|
||||
`,
|
||||
|
||||
bash: `echo "Hello world"
|
||||
`,
|
||||
|
||||
powershell: `Write-Output "Hello world"`,
|
||||
};
|
||||
@@ -95,7 +95,16 @@ export async function requireLogin(
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchVersion(baseUrl: string): Promise<string> {
|
||||
export async function tryResolveVersion(
|
||||
opts: GlobalOptions
|
||||
): Promise<number | undefined> {
|
||||
if ((opts as any).__cache_version) {
|
||||
return (opts as any).__cache_version;
|
||||
}
|
||||
|
||||
const workspaceRes = await tryResolveWorkspace(opts);
|
||||
if (workspaceRes.isError) return undefined;
|
||||
|
||||
const requestHeaders: HeadersInit = new Headers();
|
||||
|
||||
const extraHeaders = getHeaders();
|
||||
@@ -106,22 +115,10 @@ export async function fetchVersion(baseUrl: string): Promise<string> {
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
new URL(new URL(baseUrl).origin + "/api/version"),
|
||||
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
|
||||
{ headers: requestHeaders, method: "GET" }
|
||||
);
|
||||
return await response.text();
|
||||
}
|
||||
export async function tryResolveVersion(
|
||||
opts: GlobalOptions
|
||||
): Promise<number | undefined> {
|
||||
if ((opts as any).__cache_version) {
|
||||
return (opts as any).__cache_version;
|
||||
}
|
||||
|
||||
const workspaceRes = await tryResolveWorkspace(opts);
|
||||
if (workspaceRes.isError) return undefined;
|
||||
const version = await fetchVersion(workspaceRes.value.remote);
|
||||
|
||||
const version = await response.text();
|
||||
try {
|
||||
return Number.parseInt(
|
||||
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
|
||||
|
||||
35
cli/flow.ts
35
cli/flow.ts
@@ -1,6 +1,6 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { GlobalOptions, isSuperset } from "./types.ts";
|
||||
import { SEP, log, yamlStringify } from "./deps.ts";
|
||||
import { SEP, log } from "./deps.ts";
|
||||
import {
|
||||
colors,
|
||||
Command,
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "./deps.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import { resolve, track_job } from "./script.ts";
|
||||
import { defaultFlowDefinition } from "./bootstrap/flow_bootstrap.ts";
|
||||
|
||||
export interface FlowFile {
|
||||
summary: string;
|
||||
@@ -194,31 +193,6 @@ async function run(
|
||||
log.info(jobInfo.result ?? {});
|
||||
}
|
||||
|
||||
export function bootstrap(opts: GlobalOptions & {summary: string, description: string}, flowPath: string) {
|
||||
if (!validatePath(flowPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const flowDirFullPath = `${flowPath}.flow`
|
||||
Deno.mkdirSync(flowDirFullPath, { recursive: false });
|
||||
|
||||
const newFlowDefinition = defaultFlowDefinition();
|
||||
if (opts.summary !== undefined) {
|
||||
newFlowDefinition.summary = opts.summary;
|
||||
}
|
||||
if (opts.description !== undefined) {
|
||||
newFlowDefinition.description = opts.description;
|
||||
}
|
||||
|
||||
const newFlowDefinitionYaml = yamlStringify(newFlowDefinition as Record<string, any>);
|
||||
|
||||
const flowYamlPath = `${flowDirFullPath}/flow.yaml`;
|
||||
Deno.writeTextFile(
|
||||
flowYamlPath,
|
||||
newFlowDefinitionYaml,
|
||||
{ createNew: true });
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("flow related commands")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
@@ -239,11 +213,6 @@ const command = new Command()
|
||||
"-s --silent",
|
||||
"Do not ouput anything other then the final output. Useful for scripting."
|
||||
)
|
||||
.action(run as any)
|
||||
.command("bootstrap", "create a new empty flow")
|
||||
.arguments("<flow_path:string>")
|
||||
.option("--summary <summary:string>", "script summary")
|
||||
.option("--description <description:string>", "script description")
|
||||
.action(bootstrap as any);
|
||||
.action(run as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const VERSION = "v1.246.10";
|
||||
export const VERSION = "v1.236.2";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
|
||||
528
cli/script.ts
528
cli/script.ts
@@ -13,29 +13,8 @@ import {
|
||||
Table,
|
||||
writeAllSync,
|
||||
yamlParse,
|
||||
yamlStringify,
|
||||
} from "./deps.ts";
|
||||
import { deepEqual } from "./utils.ts";
|
||||
import {
|
||||
defaultScriptMetadata,
|
||||
scriptBootstrapCode,
|
||||
} from "./bootstrap/script_bootstrap.ts";
|
||||
import {
|
||||
instantiate as instantiateWasm,
|
||||
parse_bash,
|
||||
parse_bigquery,
|
||||
parse_deno,
|
||||
parse_go,
|
||||
parse_graphql,
|
||||
parse_mssql,
|
||||
parse_mysql,
|
||||
parse_powershell,
|
||||
parse_python,
|
||||
parse_snowflake,
|
||||
parse_sql,
|
||||
} from "./wasm/windmill_parser_wasm.generated.js";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
import { SchemaProperty } from "./bootstrap/common.ts";
|
||||
|
||||
export interface ScriptFile {
|
||||
parent_hash?: string;
|
||||
@@ -67,77 +46,28 @@ async function push(opts: PushOptions, filePath: string) {
|
||||
}
|
||||
|
||||
await requireLogin(opts);
|
||||
await handleFile(filePath, workspace.workspaceId, [], undefined, false);
|
||||
await handleFile(filePath, workspace.workspaceId, []);
|
||||
log.info(colors.bold.underline.green(`Script ${filePath} pushed`));
|
||||
}
|
||||
|
||||
export async function handleScriptMetadata(
|
||||
path: string,
|
||||
workspace: string,
|
||||
alreadySynced: string[],
|
||||
message: string | undefined,
|
||||
lockfileUseArray: boolean
|
||||
alreadySynced: string[]
|
||||
): Promise<boolean> {
|
||||
if (path.endsWith(".script.json") || path.endsWith(".script.yaml")) {
|
||||
const contentPath = await findContentFile(path);
|
||||
return handleFile(
|
||||
contentPath,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
message,
|
||||
lockfileUseArray
|
||||
);
|
||||
return handleFile(contentPath, workspace, alreadySynced);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function parseMetadataFile(
|
||||
scriptPath: string
|
||||
): Promise<{ isJson: boolean; payload: any } | undefined> {
|
||||
let metadataFilePath = scriptPath + ".script.json";
|
||||
try {
|
||||
await Deno.stat(metadataFilePath);
|
||||
return {
|
||||
payload: JSON.parse(await Deno.readTextFile(metadataFilePath)),
|
||||
isJson: true,
|
||||
};
|
||||
} catch {
|
||||
try {
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
await Deno.stat(metadataFilePath);
|
||||
let payload: any = yamlParse(await Deno.readTextFile(metadataFilePath));
|
||||
if (Array.isArray(payload?.["lock"])) {
|
||||
payload["lock"] = payload["lock"].join("\n");
|
||||
}
|
||||
return {
|
||||
payload,
|
||||
isJson: false,
|
||||
};
|
||||
} catch {
|
||||
// no metadata file at all. Create it
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
const scriptInitialMetadata = defaultScriptMetadata();
|
||||
const scriptInitialMetadataYaml = yamlStringify(
|
||||
scriptInitialMetadata as Record<string, any>
|
||||
);
|
||||
Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
|
||||
createNew: true,
|
||||
});
|
||||
return {
|
||||
payload: scriptInitialMetadata,
|
||||
isJson: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleFile(
|
||||
path: string,
|
||||
workspace: string,
|
||||
alreadySynced: string[],
|
||||
message: string | undefined,
|
||||
lockfileUseArray: boolean
|
||||
message?: string
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!path.includes(".inline_script.") &&
|
||||
@@ -158,7 +88,21 @@ export async function handleFile(
|
||||
const remotePath = path
|
||||
.substring(0, path.indexOf("."))
|
||||
.replaceAll("\\", "/");
|
||||
const typed = (await parseMetadataFile(remotePath))?.payload;
|
||||
const metaPath = remotePath + ".script.json";
|
||||
let typed = undefined;
|
||||
try {
|
||||
await Deno.stat(metaPath);
|
||||
typed = JSON.parse(await Deno.readTextFile(metaPath));
|
||||
} catch {
|
||||
const metaPath = remotePath + ".script.yaml";
|
||||
try {
|
||||
await Deno.stat(metaPath);
|
||||
typed = yamlParse(await Deno.readTextFile(metaPath));
|
||||
} catch {
|
||||
// no meta file
|
||||
}
|
||||
}
|
||||
|
||||
const language = inferContentTypeFromFilePath(path);
|
||||
|
||||
let remote = undefined;
|
||||
@@ -179,13 +123,11 @@ export async function handleFile(
|
||||
typed == undefined ||
|
||||
(typed.description === remote.description &&
|
||||
typed.summary === remote.summary &&
|
||||
(typed.is_template ?? false) === (remote.is_template ?? false) &&
|
||||
typed.is_template === remote.is_template &&
|
||||
typed.kind == remote.kind &&
|
||||
!remote.archived &&
|
||||
(Array.isArray(remote?.lock)
|
||||
? remote?.lock?.join("\n")
|
||||
: remote?.lock ?? ""
|
||||
).trim() == (typed?.lock ?? "").trim() &&
|
||||
(remote?.lock ?? "").trim() ==
|
||||
(typed.lock?.join("\n") ?? "").trim() &&
|
||||
deepEqual(typed.schema, remote.schema) &&
|
||||
typed.tag == remote.tag &&
|
||||
(typed.ws_error_handler_muted ?? false) ==
|
||||
@@ -214,7 +156,7 @@ export async function handleFile(
|
||||
summary: typed?.summary ?? "",
|
||||
is_template: typed?.is_template,
|
||||
kind: typed?.kind,
|
||||
lock: lockfileUseArray ? typed?.lock.split("\n") : typed?.lock,
|
||||
lock: typed?.lock,
|
||||
parent_hash: remote.hash,
|
||||
schema: typed?.schema,
|
||||
tag: typed?.tag,
|
||||
@@ -241,7 +183,7 @@ export async function handleFile(
|
||||
summary: typed?.summary ?? "",
|
||||
is_template: typed?.is_template,
|
||||
kind: typed?.kind,
|
||||
lock: lockfileUseArray ? typed?.lock.split("\n") : typed?.lock,
|
||||
lock: typed?.lock,
|
||||
parent_hash: undefined,
|
||||
schema: typed?.schema,
|
||||
tag: typed?.tag,
|
||||
@@ -314,7 +256,9 @@ export async function findContentFile(filePath: string) {
|
||||
return validCandidates[0];
|
||||
}
|
||||
|
||||
type ScriptLanguage =
|
||||
export function inferContentTypeFromFilePath(
|
||||
contentPath: string
|
||||
):
|
||||
| "python3"
|
||||
| "deno"
|
||||
| "bun"
|
||||
@@ -327,45 +271,7 @@ type ScriptLanguage =
|
||||
| "bigquery"
|
||||
| "snowflake"
|
||||
| "mssql"
|
||||
| "graphql";
|
||||
|
||||
export function filePathExtensionFromContentType(
|
||||
language: ScriptLanguage
|
||||
): string {
|
||||
if (language === "python3") {
|
||||
return ".py";
|
||||
} else if (language === "nativets") {
|
||||
return ".fetch.ts";
|
||||
} else if (language === "bun") {
|
||||
return ".bun.ts";
|
||||
} else if (language === "deno") {
|
||||
return ".ts";
|
||||
} else if (language === "go") {
|
||||
return ".go";
|
||||
} else if (language === "mysql") {
|
||||
return ".my.sql";
|
||||
} else if (language === "bigquery") {
|
||||
return ".bq.sql";
|
||||
} else if (language === "snowflake") {
|
||||
return ".sf.sql";
|
||||
} else if (language === "mssql") {
|
||||
return ".ms.sql";
|
||||
} else if (language === "postgresql") {
|
||||
return ".pg.sql";
|
||||
} else if (language === "graphql") {
|
||||
return ".gql";
|
||||
} else if (language === "bash") {
|
||||
return ".sh";
|
||||
} else if (language === "powershell") {
|
||||
return ".ps1";
|
||||
} else {
|
||||
throw new Error("Invalid language: " + language);
|
||||
}
|
||||
}
|
||||
|
||||
export function inferContentTypeFromFilePath(
|
||||
contentPath: string
|
||||
): ScriptLanguage {
|
||||
| "graphql" {
|
||||
if (contentPath.endsWith(".py")) {
|
||||
return "python3";
|
||||
} else if (contentPath.endsWith("fetch.ts")) {
|
||||
@@ -399,205 +305,6 @@ export function inferContentTypeFromFilePath(
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// below functions copied from Windmill's FE inferArgs function. TODO: refactor //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
export function inferSchema(
|
||||
language: ScriptLanguage,
|
||||
content: string,
|
||||
currentSchema: any
|
||||
) {
|
||||
let inferedSchema: any;
|
||||
if (language === "python3") {
|
||||
inferedSchema = JSON.parse(parse_python(content));
|
||||
} else if (language === "nativets") {
|
||||
inferedSchema = JSON.parse(parse_deno(content));
|
||||
} else if (language === "bun") {
|
||||
inferedSchema = JSON.parse(parse_deno(content));
|
||||
} else if (language === "deno") {
|
||||
inferedSchema = JSON.parse(parse_deno(content));
|
||||
} else if (language === "go") {
|
||||
inferedSchema = JSON.parse(parse_go(content));
|
||||
} else if (language === "mysql") {
|
||||
inferedSchema = JSON.parse(parse_mysql(content));
|
||||
inferedSchema.args = [
|
||||
{ name: "database", typ: { resource: "mysql" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "bigquery") {
|
||||
inferedSchema = JSON.parse(parse_bigquery(content));
|
||||
inferedSchema.args = [
|
||||
{ name: "database", typ: { resource: "bigquery" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "snowflake") {
|
||||
inferedSchema = JSON.parse(parse_snowflake(content));
|
||||
inferedSchema.args = [
|
||||
{ name: "database", typ: { resource: "snowflake" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "mssql") {
|
||||
inferedSchema = JSON.parse(parse_mssql(content));
|
||||
inferedSchema.args = [
|
||||
{ name: "database", typ: { resource: "ms_sql_server" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "postgresql") {
|
||||
inferedSchema = JSON.parse(parse_sql(content));
|
||||
inferedSchema.args = [
|
||||
{ name: "database", typ: { resource: "postgresql" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "graphql") {
|
||||
inferedSchema = JSON.parse(parse_graphql(content));
|
||||
inferedSchema.args = [
|
||||
{ name: "api", typ: { resource: "graphql" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "bash") {
|
||||
inferedSchema = JSON.parse(parse_bash(content));
|
||||
} else if (language === "powershell") {
|
||||
inferedSchema = JSON.parse(parse_powershell(content));
|
||||
} else {
|
||||
throw new Error("Invalid language: " + language);
|
||||
}
|
||||
if (inferedSchema.type == "Invalid") {
|
||||
throw new Error(
|
||||
`Script invalid, it cannot be parsed to infer schema. Error was: ${inferedSchema.error}`
|
||||
);
|
||||
}
|
||||
|
||||
currentSchema.required = [];
|
||||
const oldProperties = JSON.parse(JSON.stringify(currentSchema.properties));
|
||||
currentSchema.properties = {};
|
||||
|
||||
for (const arg of inferedSchema.args) {
|
||||
if (!(arg.name in oldProperties)) {
|
||||
currentSchema.properties[arg.name] = { description: "", type: "" };
|
||||
} else {
|
||||
currentSchema.properties[arg.name] = oldProperties[arg.name];
|
||||
}
|
||||
currentSchema.properties[arg.name] = sortObject(
|
||||
currentSchema.properties[arg.name]
|
||||
);
|
||||
|
||||
argSigToJsonSchemaType(arg.typ, currentSchema.properties[arg.name]);
|
||||
|
||||
currentSchema.properties[arg.name].default = arg.default;
|
||||
|
||||
if (!arg.has_default && !currentSchema.required.includes(arg.name)) {
|
||||
currentSchema.required.push(arg.name);
|
||||
}
|
||||
}
|
||||
|
||||
return currentSchema;
|
||||
}
|
||||
|
||||
function sortObject(obj: any): any {
|
||||
return Object.keys(obj)
|
||||
.sort()
|
||||
.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: obj[key],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function argSigToJsonSchemaType(
|
||||
typ:
|
||||
| string
|
||||
| { resource: string | null }
|
||||
| {
|
||||
list:
|
||||
| string
|
||||
| { str: any }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| null;
|
||||
}
|
||||
| { str: string[] | null }
|
||||
| { object: { key: string; typ: any }[] },
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
const newS: SchemaProperty = { type: "" };
|
||||
if (typ === "int") {
|
||||
newS.type = "integer";
|
||||
} else if (typ === "float") {
|
||||
newS.type = "number";
|
||||
} else if (typ === "bool") {
|
||||
newS.type = "boolean";
|
||||
} else if (typ === "email") {
|
||||
newS.type = "string";
|
||||
newS.format = "email";
|
||||
} else if (typ === "sql") {
|
||||
newS.type = "string";
|
||||
newS.format = "sql";
|
||||
} else if (typ === "yaml") {
|
||||
newS.type = "string";
|
||||
newS.format = "yaml";
|
||||
} else if (typ === "bytes") {
|
||||
newS.type = "string";
|
||||
newS.contentEncoding = "base64";
|
||||
} else if (typ === "datetime") {
|
||||
newS.type = "string";
|
||||
newS.format = "date-time";
|
||||
} else if (typeof typ !== "string" && `object` in typ) {
|
||||
newS.type = "object";
|
||||
if (typ.object) {
|
||||
const properties: Record<string, SchemaProperty> = {};
|
||||
for (const prop of typ.object) {
|
||||
properties[prop.key] = { type: undefined };
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
||||
}
|
||||
newS.properties = properties;
|
||||
}
|
||||
} else if (typeof typ !== "string" && `str` in typ) {
|
||||
newS.type = "string";
|
||||
if (typ.str) {
|
||||
newS.enum = typ.str;
|
||||
}
|
||||
} else if (typeof typ !== "string" && `resource` in typ) {
|
||||
newS.type = "object";
|
||||
newS.format = `resource-${typ.resource}`;
|
||||
} else if (typeof typ !== "string" && `list` in typ) {
|
||||
newS.type = "array";
|
||||
if (typ.list === "int" || typ.list === "float") {
|
||||
newS.items = { type: "number" };
|
||||
} else if (typ.list === "bytes") {
|
||||
newS.items = { type: "string", contentEncoding: "base64" };
|
||||
} else if (typ.list == "string") {
|
||||
newS.items = { type: "string" };
|
||||
} else if (typ.list && typeof typ.list == "object" && "str" in typ.list) {
|
||||
newS.items = { type: "string", enum: typ.list.str };
|
||||
} else {
|
||||
newS.items = { type: "object" };
|
||||
}
|
||||
} else {
|
||||
newS.type = "object";
|
||||
}
|
||||
|
||||
if (oldS.type != newS.type) {
|
||||
for (const prop of Object.getOwnPropertyNames(newS)) {
|
||||
if (prop != "description") {
|
||||
delete oldS[prop];
|
||||
}
|
||||
}
|
||||
} else if (oldS.format == "date-time" && newS.format != "date-time") {
|
||||
delete oldS.format;
|
||||
} else if (oldS.items?.type != newS.items?.type) {
|
||||
delete oldS.items;
|
||||
}
|
||||
|
||||
Object.assign(oldS, newS);
|
||||
if (oldS.format?.startsWith("resource-") && newS.type != "object") {
|
||||
oldS.format = undefined;
|
||||
}
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// end of refactoring TODO //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
@@ -777,168 +484,6 @@ async function show(opts: GlobalOptions, path: string) {
|
||||
log.info(s.content);
|
||||
}
|
||||
|
||||
async function bootstrap(
|
||||
opts: GlobalOptions & { summary: string; description: string },
|
||||
scriptPath: string,
|
||||
language: ScriptLanguage
|
||||
) {
|
||||
if (!validatePath(scriptPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptInitialCode = scriptBootstrapCode[language];
|
||||
if (scriptInitialCode === undefined) {
|
||||
throw new Error("Language unknown");
|
||||
}
|
||||
|
||||
const extension = filePathExtensionFromContentType(language);
|
||||
const scriptCodeFileFullPath = scriptPath + extension;
|
||||
const scriptMetadataFileFullPath = scriptPath + ".script.yaml";
|
||||
|
||||
try {
|
||||
await Deno.stat(scriptCodeFileFullPath);
|
||||
await Deno.stat(scriptMetadataFileFullPath);
|
||||
throw new Error("File already exists in repository");
|
||||
} catch {
|
||||
// file does not exist, we can continue
|
||||
}
|
||||
|
||||
const scriptMetadata = defaultScriptMetadata();
|
||||
if (opts.summary !== undefined) {
|
||||
scriptMetadata.summary = opts.summary;
|
||||
}
|
||||
if (opts.description !== undefined) {
|
||||
scriptMetadata.description = opts.description;
|
||||
}
|
||||
|
||||
const scriptInitialMetadataYaml = yamlStringify(
|
||||
scriptMetadata as Record<string, any>
|
||||
);
|
||||
|
||||
Deno.writeTextFile(scriptCodeFileFullPath, scriptInitialCode, {
|
||||
createNew: true,
|
||||
});
|
||||
Deno.writeTextFile(scriptMetadataFileFullPath, scriptInitialMetadataYaml, {
|
||||
createNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function generateMetadata(
|
||||
opts: GlobalOptions & { lockOnly: boolean; schemaOnly: boolean },
|
||||
scriptPath: string
|
||||
) {
|
||||
if (!validatePath(scriptPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const language = inferContentTypeFromFilePath(scriptPath);
|
||||
|
||||
// read script metadata file
|
||||
const remotePath = scriptPath
|
||||
.substring(0, scriptPath.indexOf("."))
|
||||
.replaceAll("\\", "/");
|
||||
const metadataWithType = await parseMetadataFile(remotePath);
|
||||
if (metadataWithType === undefined) {
|
||||
throw new Error("Script metadata file does not exist at this path");
|
||||
}
|
||||
|
||||
// read script content
|
||||
const scriptContent = await Deno.readTextFile(scriptPath);
|
||||
|
||||
const metadataParsedContent = metadataWithType?.payload as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
if (!opts.lockOnly) {
|
||||
await updateScriptSchema(scriptContent, language, metadataParsedContent);
|
||||
}
|
||||
|
||||
if (!opts.schemaOnly) {
|
||||
await updateScriptLock(
|
||||
workspace,
|
||||
scriptContent,
|
||||
language,
|
||||
remotePath,
|
||||
metadataParsedContent
|
||||
);
|
||||
}
|
||||
|
||||
let metaPath = remotePath + ".script.yaml";
|
||||
let newMetadataContent = yamlStringify(metadataParsedContent);
|
||||
if (metadataWithType.isJson) {
|
||||
metaPath = remotePath + ".script.json";
|
||||
newMetadataContent = JSON.stringify(metadataParsedContent);
|
||||
}
|
||||
Deno.writeTextFile(metaPath, newMetadataContent);
|
||||
}
|
||||
|
||||
async function updateScriptSchema(
|
||||
scriptContent: string,
|
||||
language: ScriptLanguage,
|
||||
metadataContent: Record<string, any>
|
||||
): Promise<void> {
|
||||
// infer schema from script content and update it inplace
|
||||
await instantiateWasm();
|
||||
const newSchema = inferSchema(
|
||||
language,
|
||||
scriptContent,
|
||||
metadataContent.schema
|
||||
);
|
||||
metadataContent.schema = newSchema;
|
||||
}
|
||||
|
||||
async function updateScriptLock(
|
||||
workspace: Workspace,
|
||||
scriptContent: string,
|
||||
language: ScriptLanguage,
|
||||
remotePath: string,
|
||||
metadataContent: Record<string, any>
|
||||
): Promise<void> {
|
||||
// generate the script lock running a dependency job in Windmill and update it inplace
|
||||
// TODO: update this once the client is released
|
||||
const rawResponse = await fetch(
|
||||
`${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/dependencies`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `token=${workspace.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
raw_scripts: [
|
||||
{
|
||||
raw_code: scriptContent,
|
||||
language: language,
|
||||
script_path: remotePath,
|
||||
},
|
||||
],
|
||||
entrypoint: remotePath,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await rawResponse.json();
|
||||
const lock = response.lock;
|
||||
if (lock === undefined) {
|
||||
throw new Error(
|
||||
`Failed to generate lockfile. Full response was: ${JSON.stringify(
|
||||
response
|
||||
)}`
|
||||
);
|
||||
}
|
||||
metadataContent.lock = lock;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Failed to generate lockfile. Status was: ${rawResponse.statusText}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("script related commands")
|
||||
.option("--show-archived", "Enable archived scripts in output")
|
||||
@@ -960,21 +505,8 @@ const command = new Command()
|
||||
)
|
||||
.option(
|
||||
"-s --silent",
|
||||
"Do not output anything other then the final output. Useful for scripting."
|
||||
"Do not ouput anything other then the final output. Useful for scripting."
|
||||
)
|
||||
.action(run as any)
|
||||
.command("bootstrap", "create a new script")
|
||||
.arguments("<path:string> <language:string>")
|
||||
.option("--summary <summary:string>", "script summary")
|
||||
.option("--description <description:string>", "script description")
|
||||
.action(bootstrap as any)
|
||||
.command(
|
||||
"generate-metadata",
|
||||
"re-generate the metadata file updating the lock and the script schema"
|
||||
)
|
||||
.arguments("<path:string>")
|
||||
.option("--lock-only", "re-generate only the lock")
|
||||
.option("--schema-only", "re-generate only script schema")
|
||||
.action(generateMetadata as any);
|
||||
.action(run as any);
|
||||
|
||||
export default command;
|
||||
|
||||
56
cli/sync.ts
56
cli/sync.ts
@@ -1,4 +1,4 @@
|
||||
import { fetchVersion, requireLogin, resolveWorkspace } from "./context.ts";
|
||||
import { requireLogin, resolveWorkspace } from "./context.ts";
|
||||
import {
|
||||
colors,
|
||||
Command,
|
||||
@@ -383,11 +383,7 @@ async function compareDynFSElement(
|
||||
}
|
||||
|
||||
for (const [k] of Object.entries(m2)) {
|
||||
if (
|
||||
m1[k] === undefined &&
|
||||
!k?.endsWith(".script.yaml") &&
|
||||
!k?.endsWith(".script.json")
|
||||
) {
|
||||
if (m1[k] === undefined) {
|
||||
changes.push({ name: "deleted", path: k });
|
||||
}
|
||||
}
|
||||
@@ -436,7 +432,7 @@ export async function ignoreF() {
|
||||
return (p: string, isDirectory: boolean) => {
|
||||
return (
|
||||
!isWhitelisted(p) &&
|
||||
(isNotWmillFile(p, isDirectory) || (!isDirectory && ignore.denies(p)))
|
||||
(isNotWmillFile(p, isDirectory) || ignore.denies(p))
|
||||
);
|
||||
};
|
||||
} catch {
|
||||
@@ -499,6 +495,7 @@ async function pull(
|
||||
prettyChanges(changes);
|
||||
if (
|
||||
!opts.yes &&
|
||||
!opts.raw &&
|
||||
!(await Confirm.prompt({
|
||||
message: `Do you want to apply these ${changes.length} changes?`,
|
||||
default: true,
|
||||
@@ -566,7 +563,6 @@ async function pull(
|
||||
log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
}
|
||||
await Deno.writeTextFile(target, change.content);
|
||||
log.info(`Writing ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
if (!opts.raw) {
|
||||
await Deno.copyFile(target, stateTarget);
|
||||
}
|
||||
@@ -710,19 +706,6 @@ async function push(
|
||||
opts
|
||||
);
|
||||
|
||||
const version = await fetchVersion(workspace.remote);
|
||||
|
||||
log.info(colors.gray("Remote version: " + version));
|
||||
|
||||
const reducedVersion = version
|
||||
.split(" v")[1]
|
||||
.split("-")[0]
|
||||
.split(".")
|
||||
.map((v) => parseInt(v));
|
||||
const lockfileUseArray =
|
||||
reducedVersion[1] < 246 ||
|
||||
reducedVersion[1] == 246 ||
|
||||
reducedVersion[2] < 5;
|
||||
log.info(
|
||||
`remote (${workspace.name}) <- local: ${changes.length} changes to apply`
|
||||
);
|
||||
@@ -739,7 +722,6 @@ async function push(
|
||||
return;
|
||||
}
|
||||
log.info(colors.gray(`Applying changes to files ...`));
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
for await (const change of changes) {
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||
@@ -755,9 +737,7 @@ async function push(
|
||||
await handleScriptMetadata(
|
||||
change.path,
|
||||
workspace.workspaceId,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
lockfileUseArray
|
||||
alreadySynced
|
||||
)
|
||||
) {
|
||||
if (!opts.raw && stateExists) {
|
||||
@@ -769,8 +749,7 @@ async function push(
|
||||
change.path,
|
||||
workspace.workspaceId,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
lockfileUseArray
|
||||
opts.message
|
||||
)
|
||||
) {
|
||||
if (!opts.raw && stateExists) {
|
||||
@@ -808,8 +787,7 @@ async function push(
|
||||
change.path,
|
||||
workspace.workspaceId,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
lockfileUseArray
|
||||
opts.message
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
@@ -926,15 +904,8 @@ const command = new Command()
|
||||
"--fail-conflicts",
|
||||
"Error on conflicts (both remote and local have changes on the same item)"
|
||||
)
|
||||
.option(
|
||||
"--raw",
|
||||
"Push without using state, just overwrite. (Will be removed as a flag and made the default behavior in the future)"
|
||||
)
|
||||
.option("--yes", "Pull without needing confirmation")
|
||||
.option(
|
||||
"--stateful",
|
||||
"Pull using state tracking (create .wmill folder and needed for --fail-conflicts). Default currently but will change in favor of --raw"
|
||||
)
|
||||
.option("--raw", "Pull without using state, just overwrite.")
|
||||
.option("--plain-secrets", "Pull secrets as plain text")
|
||||
.option("--json", "Use JSON instead of YAML")
|
||||
.option("--skip-variables", "Skip syncing variables (including secrets)")
|
||||
@@ -951,16 +922,9 @@ const command = new Command()
|
||||
"--fail-conflicts",
|
||||
"Error on conflicts (both remote and local have changes on the same item)"
|
||||
)
|
||||
.option(
|
||||
"--raw",
|
||||
"Push without using state, just overwrite. (Will be removed as a flag and made the default behavior in the future)"
|
||||
)
|
||||
.option(
|
||||
"--stateful",
|
||||
"Pull using state tracking (use .wmill folder and needed for --fail-conflicts). Default currently but will change in favor of --raw"
|
||||
)
|
||||
.option("--skip-pull", "(stateful only) Push without pulling first")
|
||||
.option("--skip-pull", "Push without pulling first (you have pulled prior)")
|
||||
.option("--yes", "Push without needing confirmation")
|
||||
.option("--raw", "Push without using state, just overwrite.")
|
||||
.option("--plain-secrets", "Push secrets as plain text")
|
||||
.option("--json", "Use JSON instead of YAML")
|
||||
.option("--skip-variables", "Skip syncing variables (including secrets)")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user