Compare commits
355 Commits
tutorials
...
dev-docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5131ee8b89 | ||
|
|
a5f6d73f7d | ||
|
|
670c84b901 | ||
|
|
92a293488e | ||
|
|
0cd1e65e46 | ||
|
|
6aa1008933 | ||
|
|
9434bbb18b | ||
|
|
e6632a32c9 | ||
|
|
54c4d03173 | ||
|
|
3932e5dfb9 | ||
|
|
9b3d6a3dd9 | ||
|
|
58d4b556eb | ||
|
|
8552c92837 | ||
|
|
955a213a50 | ||
|
|
e82f5388b6 | ||
|
|
8a44f8e779 | ||
|
|
d45e6c94ab | ||
|
|
60da67a725 | ||
|
|
0718931616 | ||
|
|
41831d58ed | ||
|
|
36816877b4 | ||
|
|
0040e15805 | ||
|
|
72317e9b54 | ||
|
|
4cdad376b3 | ||
|
|
866228b663 | ||
|
|
7eacca4caa | ||
|
|
1526537f56 | ||
|
|
1f705cab2c | ||
|
|
f2d3c8208b | ||
|
|
1b04537c9a | ||
|
|
23e374b10d | ||
|
|
5fc72ea2e6 | ||
|
|
3f5df1ee41 | ||
|
|
3dabac153f | ||
|
|
d0e0e1fdf2 | ||
|
|
8e9c491650 | ||
|
|
df4c6289ac | ||
|
|
72c8d3921d | ||
|
|
e911869d99 | ||
|
|
e1712e63a6 | ||
|
|
2031e1ebd0 | ||
|
|
de8dc1e9cd | ||
|
|
de87d7ac27 | ||
|
|
2b003c684f | ||
|
|
3097510550 | ||
|
|
0c0b2d88cc | ||
|
|
1ffed41cf9 | ||
|
|
dac61d1c98 | ||
|
|
facb67093c | ||
|
|
341a9662b7 | ||
|
|
e80454e7fd | ||
|
|
3a232dbb57 | ||
|
|
ee45f1ca7b | ||
|
|
bbb6ee978d | ||
|
|
5961995e80 | ||
|
|
78f5fd275e | ||
|
|
a7c4c5d0a8 | ||
|
|
617220d75a | ||
|
|
09042583c7 | ||
|
|
2e80404e5e | ||
|
|
8ef29099f8 | ||
|
|
1097dccfe5 | ||
|
|
76a2a1db36 | ||
|
|
65721b3b20 | ||
|
|
7675f08b7b | ||
|
|
b962ae3578 | ||
|
|
34a8b01b76 | ||
|
|
179382afbd | ||
|
|
37ee631363 | ||
|
|
dba37c2771 | ||
|
|
33f2bad8d9 | ||
|
|
368cdefd91 | ||
|
|
266b5b00da | ||
|
|
8fe68c832b | ||
|
|
92be102a07 | ||
|
|
a344928f25 | ||
|
|
f214d5f96b | ||
|
|
4af39f081b | ||
|
|
3a6b655ba8 | ||
|
|
2f156d09bf | ||
|
|
b84be60c53 | ||
|
|
eef9017a05 | ||
|
|
eff61bb8d3 | ||
|
|
8a594a89ad | ||
|
|
fb60768cf3 | ||
|
|
8f7a11b896 | ||
|
|
0b4da1a97c | ||
|
|
f6d14f7fc3 | ||
|
|
449e7de71a | ||
|
|
95ed99a1d2 | ||
|
|
8c72722710 | ||
|
|
17176bb8d1 | ||
|
|
4fb7468cf3 | ||
|
|
51fc436456 | ||
|
|
7f9050b285 | ||
|
|
94eecea02b | ||
|
|
4ec035b09a | ||
|
|
922682c4d0 | ||
|
|
831ff60bdf | ||
|
|
b86ca29fde | ||
|
|
15c75d9d00 | ||
|
|
096bf2022c | ||
|
|
bc4dd0eeaa | ||
|
|
ae219eb3be | ||
|
|
b851a5c65a | ||
|
|
bc36f5b309 | ||
|
|
5b0a4d7838 | ||
|
|
f358aa5fe2 | ||
|
|
3e5ff8682a | ||
|
|
0cbefd8214 | ||
|
|
517b2c9cca | ||
|
|
7a9091fed6 | ||
|
|
2819b09ce5 | ||
|
|
ef0165e419 | ||
|
|
be97be2c58 | ||
|
|
daf827666b | ||
|
|
8c53598aba | ||
|
|
6f33d549f0 | ||
|
|
390a988d4c | ||
|
|
8a8316c316 | ||
|
|
c638c511ca | ||
|
|
59403fbe5d | ||
|
|
7eed0b4666 | ||
|
|
4127ffe00c | ||
|
|
3d9dfa645b | ||
|
|
7c0de93b3d | ||
|
|
de1e29492c | ||
|
|
0b8a08cb49 | ||
|
|
4f7c45118b | ||
|
|
cdbab5c807 | ||
|
|
d4927cf757 | ||
|
|
492f22526a | ||
|
|
89c2fb41dd | ||
|
|
018b051781 | ||
|
|
c19be7a2fa | ||
|
|
aa3a3f6612 | ||
|
|
5b8c6bb35d | ||
|
|
8d487c0ddb | ||
|
|
efea19496f | ||
|
|
ab99950c5d | ||
|
|
2062dc6c44 | ||
|
|
b6d5eef547 | ||
|
|
46d2c86b37 | ||
|
|
8d73c9276e | ||
|
|
57f8dd9570 | ||
|
|
5a8e00d285 | ||
|
|
f217a2c368 | ||
|
|
2779891411 | ||
|
|
dfd2abc764 | ||
|
|
624279e568 | ||
|
|
cd1f9b6baa | ||
|
|
8b4e828e64 | ||
|
|
2e7871439a | ||
|
|
82578ef836 | ||
|
|
b3254938fe | ||
|
|
71619acdfa | ||
|
|
c7506e4dae | ||
|
|
2368da2146 | ||
|
|
7fc97e274e | ||
|
|
4f75a5840a | ||
|
|
8b80b3cb74 | ||
|
|
31d0d102eb | ||
|
|
fbe5c18da0 | ||
|
|
8d0d996bbb | ||
|
|
2b09fead4f | ||
|
|
fccb3d8088 | ||
|
|
9aaeaf4ee0 | ||
|
|
01564f0a1c | ||
|
|
78085a8a12 | ||
|
|
5abd9854ad | ||
|
|
85e9aa983b | ||
|
|
9853380df6 | ||
|
|
5aa14562a0 | ||
|
|
c07a17ff8a | ||
|
|
5ac646e859 | ||
|
|
bb61cef0e5 | ||
|
|
f73664759f | ||
|
|
569a55e45b | ||
|
|
5d53967ba4 | ||
|
|
a24a3b4787 | ||
|
|
98d51e219d | ||
|
|
11431a75f4 | ||
|
|
32ef18bffe | ||
|
|
8497d1d1c0 | ||
|
|
015f8e893f | ||
|
|
07ab2dbb0a | ||
|
|
a5500ea40a | ||
|
|
5eab9431bd | ||
|
|
33c07d3e63 | ||
|
|
5f335d2464 | ||
|
|
c0d72e2881 | ||
|
|
e40f16c969 | ||
|
|
1d63877a69 | ||
|
|
6764c519b2 | ||
|
|
3463bfe36f | ||
|
|
2b31653a8a | ||
|
|
449d3ae5dd | ||
|
|
cfa3f9ce7c | ||
|
|
201aa6d088 | ||
|
|
021fa23f9f | ||
|
|
b95afaa9bb | ||
|
|
83e982e84d | ||
|
|
fa457bb709 | ||
|
|
75306c8316 | ||
|
|
eaac598af3 | ||
|
|
00b70d9aaa | ||
|
|
9b09fac27a | ||
|
|
6ed7268258 | ||
|
|
014765c83b | ||
|
|
577dec5c57 | ||
|
|
9ab087a20c | ||
|
|
1a4867302f | ||
|
|
8e3d8acc80 | ||
|
|
ac2486219c | ||
|
|
a527cb8222 | ||
|
|
da24e9ab06 | ||
|
|
4dc00c2587 | ||
|
|
45c52f7723 | ||
|
|
70a7089352 | ||
|
|
895609f0d2 | ||
|
|
dd06c05046 | ||
|
|
a86fad6a9e | ||
|
|
f7c30b5d2f | ||
|
|
b999c9894b | ||
|
|
a2de6c7d5f | ||
|
|
15812b4cec | ||
|
|
b22600e9c3 | ||
|
|
c15bc8a7bf | ||
|
|
30b8e474df | ||
|
|
f420999363 | ||
|
|
984c9a1191 | ||
|
|
a2df650936 | ||
|
|
c0076c652d | ||
|
|
addabcceb0 | ||
|
|
47a7f7163a | ||
|
|
34e25f0f96 | ||
|
|
93ce252954 | ||
|
|
d3effe953b | ||
|
|
d935dba28b | ||
|
|
58167a16cd | ||
|
|
9e9683c6f7 | ||
|
|
7511f0b18e | ||
|
|
c5d305bad8 | ||
|
|
b4008e62fd | ||
|
|
bb227b69c8 | ||
|
|
5518eab7b7 | ||
|
|
a47031a41e | ||
|
|
e193a0bcdf | ||
|
|
2df1373a69 | ||
|
|
c6bf67605d | ||
|
|
0086f99dcc | ||
|
|
bba09fdaeb | ||
|
|
942d2b2244 | ||
|
|
8a2e6365a0 | ||
|
|
527f4b543a | ||
|
|
2593218cbf | ||
|
|
bfb5c1b5a4 | ||
|
|
a7c4f1a12e | ||
|
|
7cb363845e | ||
|
|
652d3c3889 | ||
|
|
42f6d2e0ee | ||
|
|
d2cccd98e0 | ||
|
|
cc4d61b6bd | ||
|
|
fbdda1a4dd | ||
|
|
48413a78c5 | ||
|
|
be1d987b41 | ||
|
|
0f64859961 | ||
|
|
b762de1eae | ||
|
|
b00bde0a63 | ||
|
|
f972e4bb06 | ||
|
|
29b1cc6ff0 | ||
|
|
b51246411f | ||
|
|
f26c7ff62b | ||
|
|
211ad52edb | ||
|
|
1392bebf87 | ||
|
|
874cf412a1 | ||
|
|
b497c3463f | ||
|
|
fbe2f0ca93 | ||
|
|
0abacac06c | ||
|
|
8fab191a7f | ||
|
|
f8fad8326d | ||
|
|
3b84672363 | ||
|
|
e45917c020 | ||
|
|
d570ef58ac | ||
|
|
cf2d031e8e | ||
|
|
9657cc9c7e | ||
|
|
200adec32f | ||
|
|
7625782038 | ||
|
|
4242f1bb63 | ||
|
|
f1e718e718 | ||
|
|
f3dfad5b94 | ||
|
|
078cb1bf3e | ||
|
|
97e3bb4aa8 | ||
|
|
c1e43de4ea | ||
|
|
ea3dab411b | ||
|
|
a76f6f7bd9 | ||
|
|
4305670d90 | ||
|
|
597e38ef36 | ||
|
|
ca3e3624c0 | ||
|
|
c1dd35c3f0 | ||
|
|
bd927a27ed | ||
|
|
00927210fd | ||
|
|
bd3ee81b14 | ||
|
|
bac831b23c | ||
|
|
c3ba1a6ab9 | ||
|
|
52157faf72 | ||
|
|
a9e8aa0f1c | ||
|
|
50c1c614ef | ||
|
|
11567d6280 | ||
|
|
a6e1510405 | ||
|
|
b7d8fd1a4d | ||
|
|
e27de7fb5d | ||
|
|
99ec12e10c | ||
|
|
9bfd471439 | ||
|
|
dbdfd62638 | ||
|
|
6f890f2120 | ||
|
|
183a4591df | ||
|
|
646c0f23da | ||
|
|
dea12e8870 | ||
|
|
944795f6ee | ||
|
|
65d4bc519c | ||
|
|
4d3507aec2 | ||
|
|
1d395ccc17 | ||
|
|
3a7129de4b | ||
|
|
2f0acb9ffa | ||
|
|
81f989837b | ||
|
|
c71a577fea | ||
|
|
bc870bd03e | ||
|
|
751edcf9b8 | ||
|
|
c2a97c53cf | ||
|
|
eb73f2a687 | ||
|
|
cd645d0935 | ||
|
|
ac9bd7ef8c | ||
|
|
5dae6577b8 | ||
|
|
372f53b7fe | ||
|
|
5662fa0d09 | ||
|
|
c958480ce8 | ||
|
|
f0b1b1f752 | ||
|
|
b8e6767cca | ||
|
|
75f87e7e11 | ||
|
|
3e5a179eb8 | ||
|
|
c082c6350e | ||
|
|
cfd489a550 | ||
|
|
1f4ae53fb4 | ||
|
|
0dcbf270da | ||
|
|
82c139ed09 | ||
|
|
0789bef120 | ||
|
|
1a7dc0a3bd | ||
|
|
ce323709a9 | ||
|
|
61a5e1f1ac | ||
|
|
3b44f9a72c | ||
|
|
b349308ff7 | ||
|
|
f87b722a21 | ||
|
|
0e9be7f300 | ||
|
|
8681e83b57 |
7
.env
7
.env
@@ -1,2 +1,7 @@
|
|||||||
DB_PASSWORD=changeme
|
DB_PASSWORD=changeme
|
||||||
WM_BASE_URL=localhost
|
|
||||||
|
# this is the url that your instance is publicly exposed to
|
||||||
|
WM_BASE_URL=http://localhost
|
||||||
|
|
||||||
|
# this is the url that caddy will reverse proxy from. It might be different than WM_BASE_URL if you re using a second proxy/load balancer in front
|
||||||
|
CADDY_REVERSE_PROXY=http://localhost
|
||||||
|
|||||||
2
.github/change-versions.sh
vendored
2
.github/change-versions.sh
vendored
@@ -4,7 +4,7 @@ VERSION=$1
|
|||||||
echo "Updating versions to: $VERSION"
|
echo "Updating versions to: $VERSION"
|
||||||
|
|
||||||
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml
|
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml
|
||||||
sed -i -e "/^const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
|
sed -i -e "/^export const VERSION =/s/= .*/= \"v$VERSION\";/" cli/main.ts
|
||||||
sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml
|
sed -i -e "/version: /s/: .*/: $VERSION/" backend/windmill-api/openapi.yaml
|
||||||
sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml
|
sed -i -e "/version: /s/: .*/: $VERSION/" openflow.openapi.yaml
|
||||||
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json
|
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json
|
||||||
|
|||||||
45
.github/workflows/docker-image.yml
vendored
45
.github/workflows/docker-image.yml
vendored
@@ -18,7 +18,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -50,7 +50,6 @@ jobs:
|
|||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|
||||||
|
|
||||||
- name: Build and push publicly
|
- name: Build and push publicly
|
||||||
uses: depot/build-push-action@v1
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -63,7 +62,7 @@ jobs:
|
|||||||
labels: |
|
labels: |
|
||||||
${{ steps.meta-public.outputs.labels }}
|
${{ steps.meta-public.outputs.labels }}
|
||||||
org.opencontainers.image.licenses=AGPLv3
|
org.opencontainers.image.licenses=AGPLv3
|
||||||
|
|
||||||
build_ee:
|
build_ee:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
@@ -142,7 +141,28 @@ jobs:
|
|||||||
# run: docker kill ${{ steps.docker-container.outputs.id }}
|
# run: docker kill ${{ steps.docker-container.outputs.id }}
|
||||||
# if: always()
|
# if: always()
|
||||||
|
|
||||||
|
deploy_s3:
|
||||||
|
needs: [build_ee]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- uses: shrink/actions-docker-extract@v2
|
||||||
|
id: extract
|
||||||
|
with:
|
||||||
|
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
|
||||||
|
path: "/static_frontend/."
|
||||||
|
|
||||||
|
- uses: reggionick/s3-deploy@v3
|
||||||
|
with:
|
||||||
|
folder: ${{ steps.extract.outputs.destination }}
|
||||||
|
bucket: windmill-frontend
|
||||||
|
bucket-region: us-east-1
|
||||||
publish_privately_heavy:
|
publish_privately_heavy:
|
||||||
needs: [build_ee]
|
needs: [build_ee]
|
||||||
runs-on: [self-hosted, new]
|
runs-on: [self-hosted, new]
|
||||||
@@ -191,8 +211,12 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
${{ steps.meta-heavy.outputs.tags }}
|
${{ steps.meta-heavy.outputs.tags }}
|
||||||
labels: ${{ steps.meta-heavy.outputs.labels }}
|
labels: ${{ steps.meta-heavy.outputs.labels }}
|
||||||
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache
|
cache-from:
|
||||||
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache,mode=max
|
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
|
||||||
|
}}-heavy:buildcache
|
||||||
|
cache-to:
|
||||||
|
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
|
||||||
|
}}-heavy:buildcache,mode=max
|
||||||
|
|
||||||
publish_privately_helm:
|
publish_privately_helm:
|
||||||
runs-on: [self-hosted, new]
|
runs-on: [self-hosted, new]
|
||||||
@@ -204,7 +228,6 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
@@ -220,7 +243,7 @@ jobs:
|
|||||||
registry: ${{ env.ECR_REGISTRY }}
|
registry: ${{ env.ECR_REGISTRY }}
|
||||||
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
- name: Build and push privately
|
- name: Build and push privately
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
@@ -230,5 +253,9 @@ jobs:
|
|||||||
file: ./docker/DockerfileHelm
|
file: ./docker/DockerfileHelm
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:helm
|
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:helm
|
||||||
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache
|
cache-from:
|
||||||
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache,mode=max
|
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
|
||||||
|
}}-helm:buildcache
|
||||||
|
cache-to:
|
||||||
|
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
|
||||||
|
}}-helm:buildcache,mode=max
|
||||||
|
|||||||
2
.github/workflows/go_on_release.yml
vendored
2
.github/workflows/go_on_release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
- name: generate_go
|
- name: generate_go
|
||||||
run: |
|
run: |
|
||||||
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0
|
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0
|
||||||
|
|||||||
283
CHANGELOG.md
283
CHANGELOG.md
@@ -1,6 +1,289 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
|
||||||
|
## [1.87.0](https://github.com/windmill-labs/windmill/compare/v1.86.0...v2.0.0) (2023-04-11)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **frontend:** Add option to return file names ([#1380](https://github.com/windmill-labs/windmill/issues/1380))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **backend:** add instance events webhook ([f2d3c82](https://github.com/windmill-labs/windmill/commit/f2d3c8208b6daa49f304f355752145de47138a3c))
|
||||||
|
* **backend:** extend cached resolution for go ([dac61d1](https://github.com/windmill-labs/windmill/commit/dac61d1c982576d7589e16ab01c8cc8bad6e1686))
|
||||||
|
* **backend:** Redis based queue ([#1324](https://github.com/windmill-labs/windmill/issues/1324)) ([d45e6c9](https://github.com/windmill-labs/windmill/commit/d45e6c94abed609357b18d4daa7de6b2ea0ba978))
|
||||||
|
* **frontend:** Add option to return file names ([#1380](https://github.com/windmill-labs/windmill/issues/1380)) ([3dabac1](https://github.com/windmill-labs/windmill/commit/3dabac153f302f48210d15ebaec514e72717300f))
|
||||||
|
* **python:** cache dependency resolution ([facb670](https://github.com/windmill-labs/windmill/commit/facb67093ce7d3b0874d0d559fb272ed822ce360))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** nested deno relative imports ([955a213](https://github.com/windmill-labs/windmill/commit/955a213a504c1f3b8811c930823e87fe7dba101a))
|
||||||
|
* **cli:** overwrite archived scripts ([1f705ca](https://github.com/windmill-labs/windmill/commit/1f705cab2ce8c79829f22fc6af9e06ecba7450b1))
|
||||||
|
* **frontend:** Add missing stopPropagation ([#1394](https://github.com/windmill-labs/windmill/issues/1394)) ([58d4b55](https://github.com/windmill-labs/windmill/commit/58d4b556ebbd76c6f07f1a16d601a9d824b99f7e))
|
||||||
|
* **frontend:** fix app init issue ([d0e0e1f](https://github.com/windmill-labs/windmill/commit/d0e0e1fdf27d9a7fb86c66e43398786b64d8b6b7))
|
||||||
|
* **frontend:** Fix frontend dependencies ([#1379](https://github.com/windmill-labs/windmill/issues/1379)) ([8e9c491](https://github.com/windmill-labs/windmill/commit/8e9c49165060a4a7f831b8be075593f89d867784))
|
||||||
|
* **frontend:** Fix icon picker input ([#1389](https://github.com/windmill-labs/windmill/issues/1389)) ([8a44f8e](https://github.com/windmill-labs/windmill/commit/8a44f8e7796f13698e2a99af9f3772f5e676604b))
|
||||||
|
* **frontend:** Fix mac shortcuts ([#1381](https://github.com/windmill-labs/windmill/issues/1381)) ([41831d5](https://github.com/windmill-labs/windmill/commit/41831d58ed593bb283600b76170f6e76783e0eae))
|
||||||
|
* **frontend:** fix popover configuration to avoid content shift ([#1377](https://github.com/windmill-labs/windmill/issues/1377)) ([2031e1e](https://github.com/windmill-labs/windmill/commit/2031e1ebd0dc020da104ee84a0294c86babcefaf))
|
||||||
|
* **frontend:** remove stopPropagation that was preventing components dnd ([#1378](https://github.com/windmill-labs/windmill/issues/1378)) ([de8dc1e](https://github.com/windmill-labs/windmill/commit/de8dc1e9cd7beea2ce62656e9e7676214f77a110))
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* parallelize more operations for deno jobs ([e911869](https://github.com/windmill-labs/windmill/commit/e911869d990956463834ac9ff35c52ba8236e362))
|
||||||
|
|
||||||
|
## [1.86.0](https://github.com/windmill-labs/windmill/compare/v1.85.0...v1.86.0) (2023-04-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **backend:** add /ready endpoint for workers ([94eecea](https://github.com/windmill-labs/windmill/commit/94eecea02b6295ad5674db4b010bf6ab7984fa17))
|
||||||
|
* **backend:** add GET endpoint to trigger scripts ([15c75d9](https://github.com/windmill-labs/windmill/commit/15c75d9d00a69ae97123ed371b9657e298345bdb))
|
||||||
|
* **backend:** lowercase all emails in relevant endpoints ([#1361](https://github.com/windmill-labs/windmill/issues/1361)) ([7f9050b](https://github.com/windmill-labs/windmill/commit/7f9050b285cf8f7f6baf05452b673f58988c452c))
|
||||||
|
* **cli:** add getFullResource ([3a232db](https://github.com/windmill-labs/windmill/commit/3a232dbb5792c28b26747e1ba260fffcdd4a8416))
|
||||||
|
* do cache bucket syncing in background + check tar before pushing it ([#1360](https://github.com/windmill-labs/windmill/issues/1360)) ([3e5ff86](https://github.com/windmill-labs/windmill/commit/3e5ff8682a298ba9e59b2662c4c04c5698447204))
|
||||||
|
* **frontend:** add flow expand button ([34a8b01](https://github.com/windmill-labs/windmill/commit/34a8b01b762c0b210d76101e7da7bd2397258e8d))
|
||||||
|
* **frontend:** add impersonate api + local resolution of import by lsp v0 ([7675f08](https://github.com/windmill-labs/windmill/commit/7675f08b7bfe319e496a86a7ef1ab7cc8c1d12d2))
|
||||||
|
* **frontend:** add workspace to ctx ([8f7a11b](https://github.com/windmill-labs/windmill/commit/8f7a11b8964e2c3405ce3689f9cf2298f9e71c75))
|
||||||
|
* **frontend:** Improve login + toasts ([#1363](https://github.com/windmill-labs/windmill/issues/1363)) ([92be102](https://github.com/windmill-labs/windmill/commit/92be102a070b1f17b9d3e40524cd21b54301b5a7))
|
||||||
|
* **frontend:** make script editor a single page ([b84be60](https://github.com/windmill-labs/windmill/commit/b84be60c53ca1ef65826123f39099d33c1f549c0))
|
||||||
|
* **frontend:** Tone down text + display whole text ([#1366](https://github.com/windmill-labs/windmill/issues/1366)) ([f214d5f](https://github.com/windmill-labs/windmill/commit/f214d5f96b6ac26cd3ef90a6ab696a6dfe02b3f0))
|
||||||
|
* improved cron/schedule editor ([#1362](https://github.com/windmill-labs/windmill/issues/1362)) ([17176bb](https://github.com/windmill-labs/windmill/commit/17176bb8d112b35228ce9183f4b2f81abe9e5b6e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** allow cors ([8a594a8](https://github.com/windmill-labs/windmill/commit/8a594a89adba9915508884f900f58c4ab53cdfec))
|
||||||
|
* **backend:** allow longer name/company ([eff61bb](https://github.com/windmill-labs/windmill/commit/eff61bb8d3496bc1c5be4b1051f99ed4470a47ff))
|
||||||
|
* **backend:** always flush bash output ([517b2c9](https://github.com/windmill-labs/windmill/commit/517b2c9cca54628c8ee692d65c05bc2513eaaf22))
|
||||||
|
* **backend:** always flush bash output ([7a9091f](https://github.com/windmill-labs/windmill/commit/7a9091fed6aa99201b75bab88d4faddbe041eee4))
|
||||||
|
* **backend:** inline script app python fix ([8c72722](https://github.com/windmill-labs/windmill/commit/8c72722710db8e3720b01180b504cbc66e79f5ca))
|
||||||
|
* **frontend:** Add FlowGraph display on Safari ([#1351](https://github.com/windmill-labs/windmill/issues/1351)) ([2819b09](https://github.com/windmill-labs/windmill/commit/2819b09ce5011a467e994ee8b1f09cf33145003d))
|
||||||
|
* **frontend:** Fix button poppup ([#1368](https://github.com/windmill-labs/windmill/issues/1368)) ([a344928](https://github.com/windmill-labs/windmill/commit/a344928f251d697f53e40c517b0b86bd90e0ad52))
|
||||||
|
* **frontend:** Fix connected property ([#1371](https://github.com/windmill-labs/windmill/issues/1371)) ([4af39f0](https://github.com/windmill-labs/windmill/commit/4af39f081bf3d07aaade39e5a5a221741fe8f973))
|
||||||
|
* **frontend:** Fix flow templateEditor ([#1367](https://github.com/windmill-labs/windmill/issues/1367)) ([51fc436](https://github.com/windmill-labs/windmill/commit/51fc436456104c2d6a3cd6f6d62f08929e40d450))
|
||||||
|
* **frontend:** make croninput a builder rather than a tab ([266b5b0](https://github.com/windmill-labs/windmill/commit/266b5b00da3bd7643eaa5dba1b8c1456f11c5e30))
|
||||||
|
* **frontend:** Minor fixes ([#1374](https://github.com/windmill-labs/windmill/issues/1374)) ([76a2a1d](https://github.com/windmill-labs/windmill/commit/76a2a1db363facbaf9a0e9618f169d6cc66e946f))
|
||||||
|
* no need to map internal ports to hosts ([#1365](https://github.com/windmill-labs/windmill/issues/1365)) ([4ec035b](https://github.com/windmill-labs/windmill/commit/4ec035b09a58f8859bc576b03c24cc73f335f32d))
|
||||||
|
|
||||||
|
## [1.85.0](https://github.com/windmill-labs/windmill/compare/v1.84.1...v1.85.0) (2023-04-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add local cache for folder path used + invalidate cache on folder creation ([018b051](https://github.com/windmill-labs/windmill/commit/018b051781e3f40b9d1da8ccdd5edb1cd49877ba))
|
||||||
|
* **frontend:** add agGrid api hooks + ready ([de1e294](https://github.com/windmill-labs/windmill/commit/de1e29492c9aefdfc59f605ba81f7c51a96bf2f3))
|
||||||
|
* **frontend:** Add ID renaming popup ([#1344](https://github.com/windmill-labs/windmill/issues/1344)) ([0b8a08c](https://github.com/windmill-labs/windmill/commit/0b8a08cb49644da7c354c3631751e925ac5353b9))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** improve handling subflow with many depth using tailrec ([8c53598](https://github.com/windmill-labs/windmill/commit/8c53598aba3fb89f4174d1c0ab3912096ac07c96))
|
||||||
|
* **backend:** improve subflow processing ([390a988](https://github.com/windmill-labs/windmill/commit/390a988d4c96256a4fbd6a9302fc47a5648c2c43))
|
||||||
|
* **frontend:** PDF reader header positioning ([#1350](https://github.com/windmill-labs/windmill/issues/1350)) ([daf8276](https://github.com/windmill-labs/windmill/commit/daf827666b13917f8c9abeab5bb2b072bd0fef0b))
|
||||||
|
|
||||||
|
## [1.84.1](https://github.com/windmill-labs/windmill/compare/v1.84.0...v1.84.1) (2023-03-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cli:** overwrite instead of smart diff ([b6d5eef](https://github.com/windmill-labs/windmill/commit/b6d5eef5479e38cc36af2db67d4c45f78c622b9a))
|
||||||
|
|
||||||
|
## [1.84.0](https://github.com/windmill-labs/windmill/compare/v1.83.1...v1.84.0) (2023-03-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add force cancel ([fbe5c18](https://github.com/windmill-labs/windmill/commit/fbe5c18da02763371e6f32c898b31a6a29984b45))
|
||||||
|
* add the ability to edit previous versions ([2368da2](https://github.com/windmill-labs/windmill/commit/2368da214660ff1835b49b4c2c87256c9bd565cf))
|
||||||
|
* **backend:** reduce memory allocation for big forloops of flows ([c7506e4](https://github.com/windmill-labs/windmill/commit/c7506e4daec5b12bf908e6954bf6f3521a97b3ba))
|
||||||
|
* **frontend:** App component style input grouping ([#1334](https://github.com/windmill-labs/windmill/issues/1334)) ([01564f0](https://github.com/windmill-labs/windmill/commit/01564f0a1c26ee9f065bb0adeb7d5e8df0b2e5b5))
|
||||||
|
* **frontend:** Display frontend execution result in Debug Runs ([#1341](https://github.com/windmill-labs/windmill/issues/1341)) ([57f8dd9](https://github.com/windmill-labs/windmill/commit/57f8dd9570577a58fe91d93c7a9d1a9b4dc69598))
|
||||||
|
* **frontend:** improve input connection UI ([#1333](https://github.com/windmill-labs/windmill/issues/1333)) ([5ac646e](https://github.com/windmill-labs/windmill/commit/5ac646e859a07efb65542aae9365aa7791ce1097))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** add a refresh button to workspace script/hub ([bb61cef](https://github.com/windmill-labs/windmill/commit/bb61cef0e56bf7fa7f8a5f91dabd590afd5db791))
|
||||||
|
* **backend:** backend compatability on macos ([#1340](https://github.com/windmill-labs/windmill/issues/1340)) ([dfd2abc](https://github.com/windmill-labs/windmill/commit/dfd2abc76466cddca98f93fd82be91ba5d3076e0))
|
||||||
|
* **frontend:** Export python code as string ([#1339](https://github.com/windmill-labs/windmill/issues/1339)) ([2779891](https://github.com/windmill-labs/windmill/commit/277989141100b033b26b496b8a55d97d48cf7e81))
|
||||||
|
* **frontend:** improve app tables ([cd1f9b6](https://github.com/windmill-labs/windmill/commit/cd1f9b6baa0dadfb14fee3a586a4b6b164e5e402))
|
||||||
|
* **frontend:** improve loading of big args in job details ([71619ac](https://github.com/windmill-labs/windmill/commit/71619acdfac010822c1eac496a6f3f869e6ca6fb))
|
||||||
|
* **frontend:** improve loading of big jobs in run form ([b325493](https://github.com/windmill-labs/windmill/commit/b3254938fe58d8c00a0c4347e7ef519e3a6e4031))
|
||||||
|
|
||||||
|
## [1.83.1](https://github.com/windmill-labs/windmill/compare/v1.83.0...v1.83.1) (2023-03-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cli:** plain secrets might be undefined ([569a55e](https://github.com/windmill-labs/windmill/commit/569a55e45b34641b0fb4569387166f3aa89ce35f))
|
||||||
|
|
||||||
|
## [1.83.0](https://github.com/windmill-labs/windmill/compare/v1.82.0...v1.83.0) (2023-03-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **backend:** allow relative imports for python ([a5500ea](https://github.com/windmill-labs/windmill/commit/a5500ea40a77b2e0408e2a644190a8f65b18cd1d))
|
||||||
|
* **backend:** execute /bin/bash instead of /bin/sh for bash scripts ([021fa23](https://github.com/windmill-labs/windmill/commit/021fa23f9ffcd11548977a4589eb9bc2815243cf))
|
||||||
|
* **backend:** improve relative importsfor deno ([eaac598](https://github.com/windmill-labs/windmill/commit/eaac598af308cedea8f0f8fc7c189a4640b4366b))
|
||||||
|
* **backend:** increase timeout for premium workspace ([00b70d9](https://github.com/windmill-labs/windmill/commit/00b70d9aaac8ae979782492d7754060a3c2c9567))
|
||||||
|
* **frontend:** add pagination ([33c07d3](https://github.com/windmill-labs/windmill/commit/33c07d3e63f96673719ecb15e45f4cd9e18be80e))
|
||||||
|
* **frontend:** Add quick style settings to app editor ([#1308](https://github.com/windmill-labs/windmill/issues/1308)) ([ac24862](https://github.com/windmill-labs/windmill/commit/ac2486219cd91df3a7fe11d37894797a881cac6c))
|
||||||
|
* **frontend:** add recompute as a primitive ([449d3ae](https://github.com/windmill-labs/windmill/commit/449d3ae5ddeceef3fbcb7a815a4dba16c9639fb3))
|
||||||
|
* **frontend:** add textareacomponent + fix multiselect style + select multi components ([2b31653](https://github.com/windmill-labs/windmill/commit/2b31653a8aa06807678e8609cfa62cf0f2f55dce))
|
||||||
|
* **frontend:** multiselect components for apps ([577dec5](https://github.com/windmill-labs/windmill/commit/577dec5c5733cdf88e8586ce6c27159920c69c8a))
|
||||||
|
* **frontend:** use rich json editor for arrays of objects and for object in ArgInput ([b95afaa](https://github.com/windmill-labs/windmill/commit/b95afaa9bb41b102181657453a564f44f4511983))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **apps:** improve app table actionButtons behavior under many clicks ([8e3d8ac](https://github.com/windmill-labs/windmill/commit/8e3d8acc80de971ee115d6903d24864d8263f08b))
|
||||||
|
* **cli:** add --plain-secrets ([98d51e2](https://github.com/windmill-labs/windmill/commit/98d51e219df1680507114f9b57ec0b0a4a234b5c))
|
||||||
|
* **frontend:** add a modal that is always mounted to make sure compon… ([#1328](https://github.com/windmill-labs/windmill/issues/1328)) ([a527cb8](https://github.com/windmill-labs/windmill/commit/a527cb8222a2ff80dae38ebae7dc5ea0979d74c5))
|
||||||
|
* **frontend:** Disable app keyboard navigation on focused inputs ([#1326](https://github.com/windmill-labs/windmill/issues/1326)) ([da24e9a](https://github.com/windmill-labs/windmill/commit/da24e9ab0625a7503c498c179022ea4011a03170))
|
||||||
|
* **frontend:** persist description for schemas ([1a48673](https://github.com/windmill-labs/windmill/commit/1a4867302f72aaae8f422ac8f53812c116cc383d))
|
||||||
|
* **frontend:** Revert app upload input ([#1330](https://github.com/windmill-labs/windmill/issues/1330)) ([fa457bb](https://github.com/windmill-labs/windmill/commit/fa457bb7099bd31c2315eaf7f7f2c40900b2ae39))
|
||||||
|
* **frontend:** Small app fixes ([#1331](https://github.com/windmill-labs/windmill/issues/1331)) ([75306c8](https://github.com/windmill-labs/windmill/commit/75306c831616d9a01cc3a4681732aab93153f1a9))
|
||||||
|
|
||||||
|
## [1.82.0](https://github.com/windmill-labs/windmill/compare/v1.81.0...v1.82.0) (2023-03-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **backend:** introduce RESTART_ZOMBIE_JOBS and ZOMBIE_JOB_TIMEOUT ([47a7f71](https://github.com/windmill-labs/windmill/commit/47a7f7163aae3fe807e766c824085b4d1b75c8c8))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** do not consider FlowPreview as potential zombie job ([f7c30b5](https://github.com/windmill-labs/windmill/commit/f7c30b5d2f16e15f36208e07126557fd7ed84801))
|
||||||
|
* **backend:** increase dynamic js timeout + improve client passing ([34e25f0](https://github.com/windmill-labs/windmill/commit/34e25f0f96fe637cc42f4017a064c40def5d67ef))
|
||||||
|
* **cli:** improve diff speed + fix replacing cli ([b999c98](https://github.com/windmill-labs/windmill/commit/b999c9894b4011b735f37df485fe403c22c00512))
|
||||||
|
* **frontend:** Fix AppTable error display + clear errors when removing a component + properly detect that latest component run had an error ([#1322](https://github.com/windmill-labs/windmill/issues/1322)) ([c15bc8a](https://github.com/windmill-labs/windmill/commit/c15bc8a7bfb3bef2634e6093088967137cd06239))
|
||||||
|
* **frontend:** fix refresh with manual dependencies ([#1319](https://github.com/windmill-labs/windmill/issues/1319)) ([a47031a](https://github.com/windmill-labs/windmill/commit/a47031a41e6a3392101e280dcd1aea098f898447))
|
||||||
|
* **frontend:** fix settings panel ([#1323](https://github.com/windmill-labs/windmill/issues/1323)) ([30b8e47](https://github.com/windmill-labs/windmill/commit/30b8e474df5b71b7e7b36d3fe5974a289cf0dfae))
|
||||||
|
* **frontend:** Fix transformer ([#1321](https://github.com/windmill-labs/windmill/issues/1321)) ([addabcc](https://github.com/windmill-labs/windmill/commit/addabcceb0c90782ba4a934bb3822f8cc9865069))
|
||||||
|
* **frontend:** remove unnecessary div ([#1318](https://github.com/windmill-labs/windmill/issues/1318)) ([e193a0b](https://github.com/windmill-labs/windmill/commit/e193a0bcdf6690b007594d2f1325a7ec26603129))
|
||||||
|
|
||||||
|
## [1.81.0](https://github.com/windmill-labs/windmill/compare/v1.80.1...v1.81.0) (2023-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **apps:** add action on form/button/formbutton ([2593218](https://github.com/windmill-labs/windmill/commit/2593218cbf07c05521a270797055ddb22dc22b8d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **frontend:** Remove action outline on preview mode ([#1313](https://github.com/windmill-labs/windmill/issues/1313)) ([a7c4f1a](https://github.com/windmill-labs/windmill/commit/a7c4f1a12e02e8627a5955b75d572e9cf11d8122))
|
||||||
|
|
||||||
|
## [1.80.1](https://github.com/windmill-labs/windmill/compare/v1.80.0...v1.80.1) (2023-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cli:** add support for non metadataed scripts ([42f6d2e](https://github.com/windmill-labs/windmill/commit/42f6d2e0ee6294f8a1d97f5f62f2adb6edfd2fed))
|
||||||
|
|
||||||
|
## [1.80.0](https://github.com/windmill-labs/windmill/compare/v1.79.0...v1.80.0) (2023-03-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **apps:** add transformers for data sources ([0abacac](https://github.com/windmill-labs/windmill/commit/0abacac06c7dd586b48c66ff47b7589fe692205b))
|
||||||
|
* **frontend:** App set tab ([#1307](https://github.com/windmill-labs/windmill/issues/1307)) ([48413a7](https://github.com/windmill-labs/windmill/commit/48413a78c5e7e0ee8208711f15135d81136b7386))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **frontend:** add missing optional chaining ([#1306](https://github.com/windmill-labs/windmill/issues/1306)) ([29b1cc6](https://github.com/windmill-labs/windmill/commit/29b1cc6ff0ebc5edcad24a1780113889c507075d))
|
||||||
|
* **frontend:** App button triggered by ([#1304](https://github.com/windmill-labs/windmill/issues/1304)) ([cf2d031](https://github.com/windmill-labs/windmill/commit/cf2d031e8e89faa2cd7fa58436cbe7cf4d9045f9))
|
||||||
|
|
||||||
|
## [1.79.0](https://github.com/windmill-labs/windmill/compare/v1.78.0...v1.79.0) (2023-03-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **frontend:** add listeners for frontend scripts ([597e38e](https://github.com/windmill-labs/windmill/commit/597e38ef367d38fa97fc443ccb2c721e5964fece))
|
||||||
|
* **frontend:** add table actions navigation ([#1298](https://github.com/windmill-labs/windmill/issues/1298)) ([c3ba1a6](https://github.com/windmill-labs/windmill/commit/c3ba1a6ab97484a08a5a20187bb858a5af7025cb))
|
||||||
|
* **frontend:** App component triggers ([#1303](https://github.com/windmill-labs/windmill/issues/1303)) ([078cb1b](https://github.com/windmill-labs/windmill/commit/078cb1bf3e4de08cb018578f04d24392a6462f69))
|
||||||
|
* **frontend:** Component control ([#1293](https://github.com/windmill-labs/windmill/issues/1293)) ([bd927a2](https://github.com/windmill-labs/windmill/commit/bd927a27ed9581dbf67ea3694f9d989f8d71d2ed))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **frontend:** App panel styling ([#1284](https://github.com/windmill-labs/windmill/issues/1284)) ([c1dd35c](https://github.com/windmill-labs/windmill/commit/c1dd35c3f0fcbc1be43273f82a873c3c07863417))
|
||||||
|
* **frontend:** Display app context search on top ([#1300](https://github.com/windmill-labs/windmill/issues/1300)) ([bd3ee81](https://github.com/windmill-labs/windmill/commit/bd3ee81b14846f16ccd16461de99b46fe68be6ba))
|
||||||
|
* **frontend:** fix horizontal splitpanes ([#1301](https://github.com/windmill-labs/windmill/issues/1301)) ([ea3dab4](https://github.com/windmill-labs/windmill/commit/ea3dab411b3d5dd772e04c8831e789e2470aaf28))
|
||||||
|
* **frontend:** fix map render ([#1297](https://github.com/windmill-labs/windmill/issues/1297)) ([0092721](https://github.com/windmill-labs/windmill/commit/00927210fd68c31cb793ef4f0efea05711ebcf00))
|
||||||
|
* **frontend:** Hide archive toggle with empty list ([#1296](https://github.com/windmill-labs/windmill/issues/1296)) ([bac831b](https://github.com/windmill-labs/windmill/commit/bac831b23ce85a683ddbd4537900670a0b7d12a8))
|
||||||
|
|
||||||
|
## [1.78.0](https://github.com/windmill-labs/windmill/compare/v1.77.0...v1.78.0) (2023-03-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **frontend:** app textcomponent editable + tooltip ([11567d6](https://github.com/windmill-labs/windmill/commit/11567d6280ea60f1a8c3c6607c724179775cbbe3))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** whitelist for include_header was ignored in some cases ([183a459](https://github.com/windmill-labs/windmill/commit/183a4591df700ab4720de6e92a83631256940089))
|
||||||
|
* **frontend:** improve rendering performance after component moving ([6f890f2](https://github.com/windmill-labs/windmill/commit/6f890f2120885f90d986fbd655096b45bf9de539))
|
||||||
|
* **frontend:** remove staticOutputs from apps ([dbdfd62](https://github.com/windmill-labs/windmill/commit/dbdfd626386398180ecba7976714f86365eeccd8))
|
||||||
|
|
||||||
|
## [1.77.0](https://github.com/windmill-labs/windmill/compare/v1.76.0...v1.77.0) (2023-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **apps:** state can be used as input in apps ([2f0acb9](https://github.com/windmill-labs/windmill/commit/2f0acb9ffa8dace4a886527dcee49809d019b271))
|
||||||
|
* **apps:** tabs can be made pages or invisible + better frontend scripts reactivity ([cd645d0](https://github.com/windmill-labs/windmill/commit/cd645d0935f2d06e0ff71f14d2cf63accd378ff3))
|
||||||
|
* **deno:** add support for custom npm repo ([#1291](https://github.com/windmill-labs/windmill/issues/1291)) ([944795f](https://github.com/windmill-labs/windmill/commit/944795f6eeaa7d01ab1a35a80570a55c363723e6))
|
||||||
|
* **frontend:** add setTab to frontend scripts ([c2a97c5](https://github.com/windmill-labs/windmill/commit/c2a97c53cfff0fdb35dd8bc249490566eebdc1a9))
|
||||||
|
* **frontend:** app components output panel ([#1283](https://github.com/windmill-labs/windmill/issues/1283)) ([751edcf](https://github.com/windmill-labs/windmill/commit/751edcf9b8e0976a1d073603c9eff5dc6e714490))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** do not cache reference to workspace scripts ([eb73f2a](https://github.com/windmill-labs/windmill/commit/eb73f2a687f6faad301b9038ab8585450bec7481))
|
||||||
|
* **frontend:** fix app tabs ([#1288](https://github.com/windmill-labs/windmill/issues/1288)) ([c71a577](https://github.com/windmill-labs/windmill/commit/c71a577fead90c9cd01a736b54d859ec4f0b7807))
|
||||||
|
* **frontend:** fix container deletion ([#1287](https://github.com/windmill-labs/windmill/issues/1287)) ([bc870bd](https://github.com/windmill-labs/windmill/commit/bc870bd03eb76cb8bc0e0c861f6cd8a9c661186b))
|
||||||
|
* **frontend:** Update setting accordion ([#1285](https://github.com/windmill-labs/windmill/issues/1285)) ([dea12e8](https://github.com/windmill-labs/windmill/commit/dea12e8870ece998bb6607723cbaab9b9a958f22))
|
||||||
|
|
||||||
|
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **frontend:** add frontend (JS) scripts to apps ([f0b1b1f](https://github.com/windmill-labs/windmill/commit/f0b1b1f752731ba434b960a75624118152f53c00))
|
||||||
|
* **frontend:** Copy, Cut and Paste ([#1279](https://github.com/windmill-labs/windmill/issues/1279)) ([82c139e](https://github.com/windmill-labs/windmill/commit/82c139ed0992be401e250cfb7ecc0fca61b76772))
|
||||||
|
* **frontend:** disabled for action buttons can now depend on row ([75f87e7](https://github.com/windmill-labs/windmill/commit/75f87e7e1117a9c12afcf626379e94b134a9a493))
|
||||||
|
* **frontend:** improve drag-n-drop behavior ([cfd489a](https://github.com/windmill-labs/windmill/commit/cfd489a55059e7b6843f99bab261c70b3852e6a2))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** improve worker ping api ([c958480](https://github.com/windmill-labs/windmill/commit/c958480ce83844a989f58dd5a70eb288582e2194))
|
||||||
|
* **frontend:** General fixes and updates ([#1281](https://github.com/windmill-labs/windmill/issues/1281)) ([3e5a179](https://github.com/windmill-labs/windmill/commit/3e5a179eb8cd8001f49c92305141dade1571e20f))
|
||||||
|
|
||||||
|
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
|
||||||
|
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
|
||||||
|
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
|
||||||
|
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
|
||||||
|
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
|
||||||
|
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
|
||||||
|
|
||||||
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
|
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
Caddyfile
14
Caddyfile
@@ -1,15 +1,5 @@
|
|||||||
{
|
{$BASE_URL} {
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
http://{$BASE_URL} {
|
|
||||||
bind {$ADDRESS}
|
|
||||||
bind {$ADDRESS}
|
bind {$ADDRESS}
|
||||||
reverse_proxy /ws/* http://lsp:3001
|
reverse_proxy /ws/* http://lsp:3001
|
||||||
reverse_proxy /* http://windmill_server:8000
|
reverse_proxy /* http://windmill_server:8000
|
||||||
|
}
|
||||||
https://{$BASE_URL} {
|
|
||||||
bind {$ADDRESS}
|
|
||||||
reverse_proxy /ws/* http://localhost:3001
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,7 @@ COPY .git/ .git/
|
|||||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
|
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
|
||||||
|
|
||||||
|
|
||||||
FROM python:3.11.2-slim-buster
|
FROM python:3.11.3-slim-buster
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
ARG APP=/usr/src/app
|
ARG APP=/usr/src/app
|
||||||
@@ -124,6 +124,7 @@ ENV TZ=Etc/UTC
|
|||||||
|
|
||||||
RUN /usr/local/bin/python3 -m pip install pip-tools
|
RUN /usr/local/bin/python3 -m pip install pip-tools
|
||||||
|
|
||||||
|
COPY --from=frontend /frontend/build /static_frontend
|
||||||
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
|
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
|
||||||
|
|
||||||
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||||
|
|||||||
93
README.md
93
README.md
@@ -26,7 +26,8 @@ Open-source developer infrastructure for internal tools. Self-hostable alternati
|
|||||||
|
|
||||||
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
|
# Windmill - Turn scripts into workflows and UIs that you can share and run at scale
|
||||||
|
|
||||||
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers dedicated instance and commercial support and licenses.
|
Windmill is <b>fully open-sourced (AGPLv3)</b> and Windmill Labs offers
|
||||||
|
dedicated instance and commercial support and licenses.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -69,12 +70,12 @@ https://user-images.githubusercontent.com/275584/218350457-bc2fdc3b-e667-4da5-a2
|
|||||||
|
|
||||||
3. Make it flow! You can chain your scripts or scripts made by the community
|
3. Make it flow! You can chain your scripts or scripts made by the community
|
||||||
shared on [WindmillHub](https://hub.windmill.dev).
|
shared on [WindmillHub](https://hub.windmill.dev).
|
||||||

|

|
||||||
|
|
||||||
4. Build complex UI on top of your scripts and flows.
|
4. Build complex UI on top of your scripts and flows.
|
||||||

|

|
||||||
|
|
||||||
Scripts and flows can also be triggered by a cron schedule '*/5 * * * *' or
|
Scripts and flows can also be triggered by a cron schedule '_/5 _ \* \* \*' or
|
||||||
through webhooks.
|
through webhooks.
|
||||||
|
|
||||||
You can build your entire infra on top of Windmill!
|
You can build your entire infra on top of Windmill!
|
||||||
@@ -82,46 +83,49 @@ You can build your entire infra on top of Windmill!
|
|||||||
## Show me some actual script code
|
## Show me some actual script code
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import * as wmill from "https://deno.land/x/windmill@v1.62.0/mod.ts"
|
import * as wmill from "https://deno.land/x/windmill@v1.62.0/mod.ts";
|
||||||
//import any dependency from npm
|
//import any dependency from npm
|
||||||
|
|
||||||
import cowsay from 'npm:cowsay@1.5.0'
|
import cowsay from "npm:cowsay@1.5.0";
|
||||||
|
|
||||||
export async function main(
|
export async function main(
|
||||||
a: number,
|
a: number,
|
||||||
// unions generate enums
|
// unions generate enums
|
||||||
b: "my" | "enum",
|
b: "my" | "enum",
|
||||||
// default parameters prefill the field
|
// default parameters prefill the field
|
||||||
d = "default arg",
|
d = "default arg",
|
||||||
// nested objects work c = { nested: "object" },
|
// nested objects work c = { nested: "object" },
|
||||||
// permissioned and typed json
|
// permissioned and typed json
|
||||||
db: wmill.Resource<"postgresql">) {
|
db: wmill.Resource<"postgresql">
|
||||||
|
) {
|
||||||
|
const email = Deno.env.get("WM_EMAIL");
|
||||||
|
// variables are permissioned and by path
|
||||||
|
let variable = await wmill.getVariable("f/company-folder/my_secret");
|
||||||
|
const lastTimeRun = await wmill.getState();
|
||||||
|
// logs are printed and always inspectable
|
||||||
|
console.log(cowsay.say({ text: "hello " + email + " " + lastTimeRun }));
|
||||||
|
await wmill.setState(Date.now());
|
||||||
|
|
||||||
const email = Deno.env.get('WM_EMAIL')
|
// return is serialized as JSON
|
||||||
// variables are permissioned and by path
|
return { foo: d, variable };
|
||||||
let variable = await wmill.getVariable('f/company-folder/my_secret')
|
|
||||||
const lastTimeRun = await wmill.getState()
|
|
||||||
// logs are printed and always inspectable
|
|
||||||
console.log(cowsay.say({ text: "hello " + email + " " + lastTimeRun }))
|
|
||||||
await wmill.setState(Date.now())
|
|
||||||
|
|
||||||
// return is serialized as JSON
|
|
||||||
return { foo: d, variable };
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
We have a powerful CLI to interact with the windmill platform and sync your
|
We have a powerful CLI to interact with the windmill platform and sync your
|
||||||
scripts from local files, github repos and to run scripts and flows on the instance from local commands. See
|
scripts from local files, github repos and to run scripts and flows on the
|
||||||
|
instance from local commands. See
|
||||||
[more details](https://github.com/windmill-labs/windmill/tree/main/cli)
|
[more details](https://github.com/windmill-labs/windmill/tree/main/cli)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### Running scripts locally
|
### Running scripts locally
|
||||||
|
|
||||||
You can run your script locally easily, you simply need to pass the right environment variables for the `wmill` client library to fetch resource and variables from your instance if necessary. See more: <https://docs.windmill.dev/docs/advanced/local_development/>
|
You can run your script locally easily, you simply need to pass the right
|
||||||
|
environment variables for the `wmill` client library to fetch resources and
|
||||||
|
variables from your instance if necessary. See more:
|
||||||
|
<https://docs.windmill.dev/docs/advanced/local_development/>
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@@ -182,15 +186,19 @@ compiling from source or using without a postgres super user, see
|
|||||||
|
|
||||||
### Docker compose
|
### Docker compose
|
||||||
|
|
||||||
`docker compose up` with the following docker-compose is sufficient:
|
```
|
||||||
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
|
curl https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml -o docker-compose.yml
|
||||||
|
curl https://github.com/windmill-labs/windmill/blob/main/CaddyFile -o Caddyfile
|
||||||
|
curl https://github.com/windmill-labs/windmill/blob/main/.env -o .env
|
||||||
|
|
||||||
|
docker compose up -d --pull always
|
||||||
|
```
|
||||||
|
|
||||||
Go to http://localhost et voilà :)
|
Go to http://localhost et voilà :)
|
||||||
|
|
||||||
|
|
||||||
The default super-admin user is: admin@windmill.dev / changeme
|
The default super-admin user is: admin@windmill.dev / changeme
|
||||||
|
|
||||||
From there, you can create other users (do not forget to change the password!)
|
From there, you can follow the setup app and creat other users.
|
||||||
|
|
||||||
### Kubernetes (k8s) and Helm charts
|
### Kubernetes (k8s) and Helm charts
|
||||||
|
|
||||||
@@ -199,9 +207,9 @@ We publish helm charts at:
|
|||||||
|
|
||||||
### Postgres without superuser
|
### Postgres without superuser
|
||||||
|
|
||||||
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a postgres superuser,
|
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a
|
||||||
you can run `./init-db-as-superuser.sql` to init the required users for windmill.
|
postgres superuser, you can run `./init-db-as-superuser.sql` to init the
|
||||||
|
required users for windmill.
|
||||||
|
|
||||||
### Commercial license
|
### Commercial license
|
||||||
|
|
||||||
@@ -275,8 +283,8 @@ You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
|
|||||||
### Resource types
|
### Resource types
|
||||||
|
|
||||||
You will also want to import all the approved resource types from
|
You will also want to import all the approved resource types from
|
||||||
[WindmillHub](https://hub.windmill.dev). A setup script will prompt
|
[WindmillHub](https://hub.windmill.dev). A setup script will prompt you to have
|
||||||
you to have it being synced automatically everyday.
|
it being synced automatically everyday.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -285,14 +293,16 @@ you to have it being synced automatically everyday.
|
|||||||
| DATABASE_URL | | The Postgres database url. | All |
|
| DATABASE_URL | | The Postgres database url. | All |
|
||||||
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
|
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
|
||||||
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
|
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
|
||||||
| PORT | 8000 | Exposed port | Server | |
|
| PORT | 8000 | Exposed port | Server |
|
||||||
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
|
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
|
||||||
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
|
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
|
||||||
| METRICS_ADDR | None | The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
|
| METRICS_ADDR | None | The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
|
||||||
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
|
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
|
||||||
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance | Server |
|
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance | Server |
|
||||||
| BASE_INTERNAL_URL | http://localhost:8000 | The base url that is reachable by your workers to talk to the Servers. This help avoiding going through the external load balancer for VPC-internal requests. | Worker |
|
| BASE_INTERNAL_URL | http://localhost:8000 | The base url that is reachable by your workers to talk to the Servers. This help avoiding going through the external load balancer for VPC-internal requests. | Worker |
|
||||||
| TIMEOUT | 300 | The timeout in seconds for the execution of a script | Worker |
|
| TIMEOUT | 300 | The maximum time of execution of a script. When reached, the job is failed as having timedout. | Worker |
|
||||||
|
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
|
||||||
|
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
|
||||||
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
|
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
|
||||||
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
|
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
|
||||||
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
|
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
|
||||||
@@ -301,10 +311,12 @@ you to have it being synced automatically everyday.
|
|||||||
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
|
| S3_CACHE_BUCKET (EE only) | None | The S3 bucket to sync the cache of the workers to | Worker |
|
||||||
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
|
| TAR_CACHE_RATE (EE only) | 100 | The rate at which to tar the cache of the workers. 100 means every 100th job in average (uniformly randomly distributed). | Worker |
|
||||||
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
|
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See [Slack documentation](https://api.slack.com/authentication/verifying-requests-from-slack) | Server |
|
||||||
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server | |
|
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
|
||||||
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
|
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
|
||||||
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
|
| PYTHON_PATH | /usr/local/bin/python3 | The path to the python binary. | Worker |
|
||||||
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
|
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
|
||||||
|
| GOPRIVATE | | The GOPRIVATE env variable to use private go modules | Worker |
|
||||||
|
| NETRC | | The netrc content to use a private go registry | Worker |
|
||||||
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
|
| PIP_INDEX_URL | None | The index url to pass for pip. | Worker |
|
||||||
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
|
| PIP_EXTRA_INDEX_URL | None | The extra index url to pass to pip. | Worker |
|
||||||
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
|
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
|
||||||
@@ -316,12 +328,13 @@ you to have it being synced automatically everyday.
|
|||||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
| 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_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_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
|
||||||
|
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
|
||||||
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
|
| 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 |
|
| 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 |
|
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
|
||||||
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
|
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
|
||||||
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |
|
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |
|
||||||
| NEW_USER_WEBHOOK | None | Webhook to notify of a new user added, signup/invite. Can hook back to windmill to send emails | Server |
|
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails | Server |
|
||||||
|
|
||||||
## Run a local dev setup
|
## Run a local dev setup
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,14 @@ rustflags = [
|
|||||||
]
|
]
|
||||||
incremental = true
|
incremental = true
|
||||||
|
|
||||||
|
[target.x86_64-apple-darwin]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=-undefined",
|
||||||
|
"-C", "link-arg=dynamic_lookup",
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.aarch64-apple-darwin]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=-undefined",
|
||||||
|
"-C", "link-arg=dynamic_lookup",
|
||||||
|
]
|
||||||
1280
backend/Cargo.lock
generated
1280
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "windmill"
|
name = "windmill"
|
||||||
version = "1.74.2"
|
version = "1.87.0"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.74.2"
|
version = "1.87.0"
|
||||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -28,11 +28,7 @@ name = "windmill"
|
|||||||
path = "./src/main.rs"
|
path = "./src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
enterprise = [
|
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
|
||||||
"windmill-worker/enterprise",
|
|
||||||
"windmill-queue/enterprise",
|
|
||||||
"windmill-api/enterprise",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@@ -52,12 +48,16 @@ git-version.workspace = true
|
|||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
|
rsmq_async.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
windmill-queue.workspace = true
|
windmill-queue.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
windmill-api = { path = "./windmill-api" }
|
windmill-api = { path = "./windmill-api" }
|
||||||
@@ -76,7 +76,7 @@ headers = "^0"
|
|||||||
hyper = { version = "^0", features = ["full"] }
|
hyper = { version = "^0", features = ["full"] }
|
||||||
tokio = { version = "^1", features = ["full", "tracing"] }
|
tokio = { version = "^1", features = ["full", "tracing"] }
|
||||||
tower = "^0"
|
tower = "^0"
|
||||||
tower-http = { version = "^0", features = ["trace"] }
|
tower-http = { version = "^0", features = ["trace", "cors"] }
|
||||||
tower-cookies = "^0"
|
tower-cookies = "^0"
|
||||||
serde = "^1"
|
serde = "^1"
|
||||||
serde_json = { version = "^1", features = ["preserve_order"] }
|
serde_json = { version = "^1", features = ["preserve_order"] }
|
||||||
@@ -84,6 +84,7 @@ uuid = { version = "^1", features = ["serde", "v4"] }
|
|||||||
thiserror = "^1"
|
thiserror = "^1"
|
||||||
anyhow = "^1"
|
anyhow = "^1"
|
||||||
chrono = { version = "^0", features = ["serde"] }
|
chrono = { version = "^0", features = ["serde"] }
|
||||||
|
chrono-tz = "^0"
|
||||||
tracing = "^0"
|
tracing = "^0"
|
||||||
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
|
||||||
prometheus = { version = "^0", default-features = false }
|
prometheus = { version = "^0", default-features = false }
|
||||||
@@ -122,7 +123,7 @@ itertools = "^0"
|
|||||||
regex = "^1"
|
regex = "^1"
|
||||||
deno_core = "^0"
|
deno_core = "^0"
|
||||||
async-recursion = "^1"
|
async-recursion = "^1"
|
||||||
swc_common = "^0"
|
swc_common = "0.29.39"
|
||||||
swc_ecma_parser = "0.128.2"
|
swc_ecma_parser = "0.128.2"
|
||||||
swc_ecma_ast = "0.98.1"
|
swc_ecma_ast = "0.98.1"
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
@@ -142,6 +143,7 @@ sqlx = { version = "^0", features = [
|
|||||||
dotenv = "^0"
|
dotenv = "^0"
|
||||||
ulid = { version = "^1", features = ["uuid"] }
|
ulid = { version = "^1", features = ["uuid"] }
|
||||||
futures = "^0"
|
futures = "^0"
|
||||||
|
futures-core = "^0"
|
||||||
tokio-metrics = "0.1.0"
|
tokio-metrics = "0.1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
serde_derive = "1.0.147"
|
serde_derive = "1.0.147"
|
||||||
@@ -153,3 +155,8 @@ async-stripe = { version = "0.14", features = [
|
|||||||
"checkout",
|
"checkout",
|
||||||
] }
|
] }
|
||||||
async_zip = { version = "0.0.11", features = ["full"] }
|
async_zip = { version = "0.0.11", features = ["full"] }
|
||||||
|
once_cell = "1.17.1"
|
||||||
|
rsmq_async = { version = "5.1.5" }
|
||||||
|
gosyn = "0.2.2"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Add down migration script here
|
||||||
1143
backend/migrations/20230327115727_update_app_setup.up.sql
Normal file
1143
backend/migrations/20230327115727_update_app_setup.up.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
-- Add down migration script here
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
CREATE POLICY see_extra_perms_user ON app FOR ALL
|
||||||
|
USING (extra_perms ? CONCAT('u/', current_setting('session.user')))
|
||||||
|
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
|
||||||
|
|
||||||
|
CREATE POLICY see_extra_perms_groups ON app FOR ALL
|
||||||
|
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||||
|
WITH CHECK (exists(
|
||||||
|
SELECT key, value FROM jsonb_each_text(extra_perms)
|
||||||
|
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||||
|
AND value::boolean));
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE schedule DROP COLUMN timezone;
|
||||||
|
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;
|
||||||
26
backend/migrations/20230405071524_schedule_timezone.up.sql
Normal file
26
backend/migrations/20230405071524_schedule_timezone.up.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
ALTER TABLE schedule ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC';
|
||||||
|
|
||||||
|
-- INSERT the correct IANA timezone string for each offset value
|
||||||
|
|
||||||
|
UPDATE schedule SET timezone = 'Pacific/Honolulu' WHERE offset_ = 600;
|
||||||
|
UPDATE schedule SET timezone = 'America/Anchorage' WHERE offset_ = 540;
|
||||||
|
UPDATE schedule SET timezone = 'America/Los_Angeles' WHERE offset_ = 480;
|
||||||
|
UPDATE schedule SET timezone = 'America/Chicago' WHERE offset_ = 360;
|
||||||
|
UPDATE schedule SET timezone = 'America/New_York' WHERE offset_ = 300;
|
||||||
|
UPDATE schedule SET timezone = 'America/Halifax' WHERE offset_ = 240;
|
||||||
|
UPDATE schedule SET timezone = 'America/Sao_Paulo' WHERE offset_ = 180;
|
||||||
|
UPDATE schedule SET timezone = 'Atlantic/South_Georgia' WHERE offset_ = 120;
|
||||||
|
UPDATE schedule SET timezone = 'Atlantic/Cape_Verde' WHERE offset_ = 60;
|
||||||
|
UPDATE schedule SET timezone = 'Europe/London' WHERE offset_ = 0;
|
||||||
|
UPDATE schedule SET timezone = 'Europe/Berlin' WHERE offset_ = -60;
|
||||||
|
UPDATE schedule SET timezone = 'Europe/Athens' WHERE offset_ = -120;
|
||||||
|
UPDATE schedule SET timezone = 'Europe/Moscow' WHERE offset_ = -180;
|
||||||
|
UPDATE schedule SET timezone = 'Asia/Dubai' WHERE offset_ = -240;
|
||||||
|
UPDATE schedule SET timezone = 'Asia/Aqtau' WHERE offset_ = -300;
|
||||||
|
UPDATE schedule SET timezone = 'Asia/Almaty' WHERE offset_ = -360;
|
||||||
|
UPDATE schedule SET timezone = 'Asia/Bangkok' WHERE offset_ = -420;
|
||||||
|
UPDATE schedule SET timezone = 'Asia/Hong_Kong' WHERE offset_ = -480;
|
||||||
|
UPDATE schedule SET timezone = 'Asia/Tokyo' WHERE offset_ = -540;
|
||||||
|
UPDATE schedule SET timezone = 'Australia/Sydney' WHERE offset_ = -600;
|
||||||
|
|
||||||
|
ALTER TABLE schedule DROP COLUMN offset_;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Add down migration script here
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
ALTER TABLE password ALTER COLUMN company TYPE VARCHAR(255);
|
||||||
|
ALTER TABLE password ALTER COLUMN name TYPE VARCHAR(255);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP TABLE pip_resolution_cache;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
CREATE TABLE pip_resolution_cache(
|
||||||
|
hash VARCHAR(255) PRIMARY KEY,
|
||||||
|
expiration TIMESTAMP NOT NULL,
|
||||||
|
lockfile TEXT NOT NULL
|
||||||
|
);
|
||||||
2
backend/migrations/20230410122342_saved_inputs.down.sql
Normal file
2
backend/migrations/20230410122342_saved_inputs.down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS input;
|
||||||
|
DROP TYPE RUNNABLE_TYPE;
|
||||||
13
backend/migrations/20230410122342_saved_inputs.up.sql
Normal file
13
backend/migrations/20230410122342_saved_inputs.up.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TYPE RUNNABLE_TYPE AS ENUM ('ScriptHash', 'ScriptPath', 'FlowPath');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS input (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
|
||||||
|
runnable_id VARCHAR(255) NOT NULL,
|
||||||
|
runnable_type RUNNABLE_TYPE NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
args JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
created_by VARCHAR(50) NOT NULL,
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
@@ -15,3 +15,4 @@ phf.workspace = true
|
|||||||
unicode-general-category.workspace = true
|
unicode-general-category.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
gosyn.workspace = true
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,347 +0,0 @@
|
|||||||
#![allow(clippy::large_enum_variant)] // TODO: we allow large enum variant for now, let's profile properly to see if we want to box.
|
|
||||||
|
|
||||||
use crate::parser_go_token::{Position, Token};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#CommentGroup
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CommentGroup {
|
|
||||||
// List []*Comment // len(List) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#FieldList
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FieldList<'a> {
|
|
||||||
pub opening: Option<Position<'a>>, // position of opening parenthesis/brace, if any
|
|
||||||
pub list: Vec<Field<'a>>, // field list; or nil
|
|
||||||
pub closing: Option<Position<'a>>, // position of closing parenthesis/brace, if any
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Field
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Field<'a> {
|
|
||||||
pub doc: Option<CommentGroup>, // associated documentation; or nil
|
|
||||||
pub names: Option<Vec<Ident<'a>>>, // field/method/(type) parameter names, or type "type"; or nil
|
|
||||||
pub type_: Option<Expr<'a>>, // field/method/parameter type, type list type; or nil
|
|
||||||
pub tag: Option<BasicLit<'a>>, // field tag; or nil
|
|
||||||
pub comment: Option<CommentGroup>, // line comments; or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#File
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct File<'a> {
|
|
||||||
// package name
|
|
||||||
pub decls: Vec<Decl<'a>>, // top-level declarations; or nil // list of all comments in the source file
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#FuncDecl
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FuncDecl<'a> {
|
|
||||||
pub doc: Option<CommentGroup>, // associated documentation; or nil
|
|
||||||
pub recv: Option<FieldList<'a>>, // receiver (methods); or nil (functions)
|
|
||||||
pub name: Ident<'a>, // function/method name
|
|
||||||
pub type_: FuncType<'a>, // function signature: type and value parameters, results, and position of "func" keyword
|
|
||||||
pub body: Option<BlockStmt<'a>>, // function body; or nil for external (non-Go) function
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#BlockStmt
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BlockStmt<'a> {
|
|
||||||
pub lbrace: Position<'a>, // position of "{"
|
|
||||||
pub list: Vec<Stmt>,
|
|
||||||
pub rbrace: Position<'a>, // position of "}", if any (may be absent due to syntax error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#FuncType
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FuncType<'a> {
|
|
||||||
pub func: Option<Position<'a>>, // position of "func" keyword (token.NoPos if there is no "func")
|
|
||||||
pub params: FieldList<'a>, // (incoming) parameters; non-nil
|
|
||||||
pub results: Option<FieldList<'a>>, // (outgoing) results; or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Ident
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Ident<'a> {
|
|
||||||
pub name_pos: Position<'a>, // identifier position
|
|
||||||
pub name: &'a str, // identifier name
|
|
||||||
pub obj: Option<Box<Object<'a>>>, // denoted object; or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ValueSpec
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ValueSpec<'a> {
|
|
||||||
pub doc: Option<CommentGroup>, // associated documentation; or nil
|
|
||||||
pub names: Vec<Ident<'a>>, // value names (len(Names) > 0)
|
|
||||||
pub type_: Option<Expr<'a>>, // value type; or nil
|
|
||||||
pub values: Option<Vec<Expr<'a>>>, // initial values; or nil
|
|
||||||
pub comment: Option<CommentGroup>, // line comments; or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#BasicLit
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BasicLit<'a> {
|
|
||||||
pub value_pos: Position<'a>, // literal position
|
|
||||||
pub kind: Token, // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
|
|
||||||
pub value: &'a str, // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f', "foo" or `\m\n\o`
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Object
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Object<'a> {
|
|
||||||
pub kind: ObjKind,
|
|
||||||
pub name: &'a str, // declared name
|
|
||||||
pub decl: Option<ObjDecl>, // corresponding Field, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope; or nil
|
|
||||||
pub data: Option<usize>, // object-specific data; or nil
|
|
||||||
pub type_: Option<()>, // placeholder for type information; may be nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Ellipsis
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Ellipsis<'a> {
|
|
||||||
pub ellipsis: Position<'a>, // position of "..."
|
|
||||||
pub elt: Option<Box<Expr<'a>>>, // ellipsis element type (parameter lists only); or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Ellipsis
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TypeAssertExpr<'a> {
|
|
||||||
pub x: Box<Expr<'a>>, // expression
|
|
||||||
pub lparen: Position<'a>, // position of "("
|
|
||||||
pub type_: Box<Expr<'a>>, // asserted type; nil means type switch X.(type)
|
|
||||||
pub rparen: Position<'a>, // position of ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#SliceExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SliceExpr<'a> {
|
|
||||||
pub x: Box<Expr<'a>>, // expression
|
|
||||||
pub lbrack: Position<'a>, // position of "["
|
|
||||||
pub low: Option<Box<Expr<'a>>>, // begin of slice range; or nil
|
|
||||||
pub high: Option<Box<Expr<'a>>>, // end of slice range; or nil
|
|
||||||
pub max: Option<Box<Expr<'a>>>, // maximum capacity of slice; or nil
|
|
||||||
pub slice3: bool, // true if 3-index slice (2 colons present)
|
|
||||||
pub rbrack: Position<'a>, // position of "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ObjKind
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ObjKind {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ObjDecl {}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Decl
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Decl<'a> {
|
|
||||||
FuncDecl(FuncDecl<'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Scope
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Scope<'a> {
|
|
||||||
pub outer: Option<Box<Scope<'a>>>,
|
|
||||||
pub objects: BTreeMap<&'a str, Object<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#GenDecl
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GenDecl<'a> {
|
|
||||||
pub doc: Option<CommentGroup>, // associated documentation; or nil
|
|
||||||
pub tok_pos: Position<'a>, // position of Tok
|
|
||||||
pub tok: Token, // IMPORT, CONST, TYPE, or VAR
|
|
||||||
pub lparen: Option<Position<'a>>, // position of '(', if any
|
|
||||||
pub specs: Vec<Spec>,
|
|
||||||
pub rparen: Option<Position<'a>>, // position of ')', if any
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#AssignStmt
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AssignStmt<'a> {
|
|
||||||
pub lhs: Vec<Expr<'a>>,
|
|
||||||
pub tok_pos: Position<'a>, // position of Tok
|
|
||||||
pub tok: Token, // assignment token, DEFINE
|
|
||||||
pub rhs: Vec<Expr<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#BinaryExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BinaryExpr<'a> {
|
|
||||||
pub x: Box<Expr<'a>>, // left operand
|
|
||||||
pub op_pos: Position<'a>, // position of Op
|
|
||||||
pub op: Token, // operator
|
|
||||||
pub y: Box<Expr<'a>>, // right operand
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ReturnStmt
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ReturnStmt<'a> {
|
|
||||||
pub return_: Position<'a>, // position of "return" keyword
|
|
||||||
pub results: Vec<Expr<'a>>, // result expressions; or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#TypeSpec
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TypeSpec<'a> {
|
|
||||||
pub doc: Option<CommentGroup>, // associated documentation; or nil
|
|
||||||
pub name: Option<Ident<'a>>, // type name
|
|
||||||
pub assign: Option<Position<'a>>, // position of '=', if any
|
|
||||||
pub type_: Expr<'a>, // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
|
|
||||||
pub comment: Option<CommentGroup>, // line comments; or nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#StructType
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct StructType<'a> {
|
|
||||||
pub struct_: Position<'a>, // position of "struct" keyword
|
|
||||||
pub fields: Option<FieldList<'a>>, // list of field declarations
|
|
||||||
pub incomplete: bool, // true if (source) fields are missing in the Fields list
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#StarExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct StarExpr<'a> {
|
|
||||||
pub star: Position<'a>, // position of "*"
|
|
||||||
pub x: Box<Expr<'a>>, // operand
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#InterfaceType
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct InterfaceType<'a> {
|
|
||||||
pub interface: Position<'a>, // position of "interface" keyword
|
|
||||||
pub methods: Option<FieldList<'a>>, // list of embedded interfaces, methods, or types
|
|
||||||
pub incomplete: bool, // true if (source) methods or types are missing in the Methods list
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#UnaryExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct UnaryExpr<'a> {
|
|
||||||
pub op_pos: Position<'a>, // position of Op
|
|
||||||
pub op: Token, // operator
|
|
||||||
pub x: Box<Expr<'a>>, // operand
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#CallExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CallExpr<'a> {
|
|
||||||
pub fun: Box<Expr<'a>>, // function expression
|
|
||||||
pub lparen: Position<'a>, // position of "("
|
|
||||||
pub args: Option<Vec<Expr<'a>>>, // function arguments; or nil
|
|
||||||
pub ellipsis: Option<Position<'a>>, // position of "..." (token.NoPos if there is no "...")
|
|
||||||
pub rparen: Position<'a>, // position of ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#SelectorExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SelectorExpr<'a> {
|
|
||||||
pub x: Box<Expr<'a>>, // expression
|
|
||||||
pub sel: Ident<'a>, // field selector
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ParenExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ParenExpr<'a> {
|
|
||||||
pub lparen: Position<'a>, // position of "("
|
|
||||||
pub x: Box<Expr<'a>>, // parenthesized expression
|
|
||||||
pub rparen: Position<'a>, // position of ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#FuncLit
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FuncLit<'a> {
|
|
||||||
pub type_: FuncType<'a>, // function type
|
|
||||||
pub body: BlockStmt<'a>, // function body
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ChanType
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ChanType<'a> {
|
|
||||||
pub begin: Position<'a>, // position of "chan" keyword or "<-" (whichever comes first)
|
|
||||||
pub arrow: Option<Position<'a>>, // position of "<-" (token.NoPos if there is no "<-")
|
|
||||||
pub dir: u8, // channel direction
|
|
||||||
pub value: Box<Expr<'a>>, // value type
|
|
||||||
}
|
|
||||||
|
|
||||||
// htt/opt/visual-studio-code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.htmlps://pkg.go.dev/go/ast#IndexExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct IndexExpr<'a> {
|
|
||||||
pub x: Box<Expr<'a>>, // expression
|
|
||||||
pub lbrack: Position<'a>, // position of "["
|
|
||||||
pub index: Box<Expr<'a>>, // index expression
|
|
||||||
pub rbrack: Position<'a>, // position of "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#MapType
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MapType<'a> {
|
|
||||||
pub map: Position<'a>,
|
|
||||||
pub key: Box<Expr<'a>>,
|
|
||||||
pub value: Box<Expr<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#CompositeLit
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CompositeLit<'a> {
|
|
||||||
pub type_: Box<Expr<'a>>, // literal type; or nil
|
|
||||||
pub lbrace: Position<'a>, // position of "{"
|
|
||||||
pub elts: Option<Vec<Expr<'a>>>, // list of composite elements; or nil
|
|
||||||
pub rbrace: Position<'a>, // position of "}"
|
|
||||||
pub incomplete: bool, // true if (source) expressions are missing in the Elts list
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#KeyValueExpr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct KeyValueExpr<'a> {
|
|
||||||
pub key: Box<Expr<'a>>,
|
|
||||||
pub colon: Position<'a>, // position of ":"
|
|
||||||
pub value: Box<Expr<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ArrayType
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ArrayType<'a> {
|
|
||||||
pub lbrack: Position<'a>, // position of "["
|
|
||||||
pub len: Option<Box<Expr<'a>>>, // Ellipsis node for [...]T array types, nil for slice types
|
|
||||||
pub elt: Box<Expr<'a>>, // element type
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#ChanDir
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ChanDir {
|
|
||||||
SEND = 1 << 0,
|
|
||||||
RECV = 1 << 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Spec
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Spec {}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Expr
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Expr<'a> {
|
|
||||||
ArrayType(ArrayType<'a>),
|
|
||||||
BasicLit(BasicLit<'a>),
|
|
||||||
BinaryExpr(BinaryExpr<'a>),
|
|
||||||
CallExpr(CallExpr<'a>),
|
|
||||||
ChanType(ChanType<'a>),
|
|
||||||
CompositeLit(CompositeLit<'a>),
|
|
||||||
Ellipsis(Ellipsis<'a>),
|
|
||||||
FuncLit(FuncLit<'a>),
|
|
||||||
FuncType(FuncType<'a>),
|
|
||||||
Ident(Ident<'a>),
|
|
||||||
IndexExpr(IndexExpr<'a>),
|
|
||||||
InterfaceType(InterfaceType<'a>),
|
|
||||||
KeyValueExpr(KeyValueExpr<'a>),
|
|
||||||
MapType(MapType<'a>),
|
|
||||||
ParenExpr(ParenExpr<'a>),
|
|
||||||
SelectorExpr(SelectorExpr<'a>),
|
|
||||||
SliceExpr(SliceExpr<'a>),
|
|
||||||
StarExpr(StarExpr<'a>),
|
|
||||||
StructType(StructType<'a>),
|
|
||||||
TypeAssertExpr(TypeAssertExpr<'a>),
|
|
||||||
UnaryExpr(UnaryExpr<'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/go/ast#Stmt
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Stmt {}
|
|
||||||
@@ -1,948 +0,0 @@
|
|||||||
// https://golang.org/ref/spec#Lexical_elements
|
|
||||||
|
|
||||||
use crate::parser_go_token::{Position, Token};
|
|
||||||
use phf::{phf_map, Map};
|
|
||||||
use std::fmt;
|
|
||||||
use unicode_general_category::{get_general_category, GeneralCategory};
|
|
||||||
|
|
||||||
pub type Step<'a> = (Position<'a>, Token, &'a str);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ScannerError {
|
|
||||||
HexadecimalNotFound,
|
|
||||||
OctalNotFound,
|
|
||||||
UnterminatedComment,
|
|
||||||
UnterminatedEscapedChar,
|
|
||||||
UnterminatedRune,
|
|
||||||
UnterminatedString,
|
|
||||||
InvalidDirective,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for ScannerError {}
|
|
||||||
|
|
||||||
impl fmt::Display for ScannerError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "scanner error: {:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, ScannerError>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Scanner<'a> {
|
|
||||||
directory: &'a str,
|
|
||||||
file: &'a str,
|
|
||||||
buffer: &'a str,
|
|
||||||
//
|
|
||||||
chars: std::iter::Peekable<std::str::Chars<'a>>,
|
|
||||||
current_char: Option<char>,
|
|
||||||
current_char_len: usize,
|
|
||||||
//
|
|
||||||
offset: usize,
|
|
||||||
line: usize,
|
|
||||||
column: usize,
|
|
||||||
start_offset: usize,
|
|
||||||
start_line: usize,
|
|
||||||
start_column: usize,
|
|
||||||
//
|
|
||||||
hide_column: bool,
|
|
||||||
insert_semi: bool,
|
|
||||||
pending_line_info: Option<LineInfo<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type LineInfo<'a> = (Option<&'a str>, usize, Option<usize>, bool);
|
|
||||||
|
|
||||||
impl<'a> Scanner<'a> {
|
|
||||||
pub fn new(filename: &'a str, buffer: &'a str) -> Self {
|
|
||||||
let (directory, file) = filename.rsplit_once('/').unwrap_or(("", filename));
|
|
||||||
let mut s = Scanner {
|
|
||||||
directory,
|
|
||||||
file,
|
|
||||||
buffer,
|
|
||||||
//
|
|
||||||
chars: buffer.chars().peekable(),
|
|
||||||
current_char: None,
|
|
||||||
current_char_len: 0,
|
|
||||||
//
|
|
||||||
offset: 0,
|
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
start_offset: 0,
|
|
||||||
start_line: 1,
|
|
||||||
start_column: 1,
|
|
||||||
//
|
|
||||||
hide_column: false,
|
|
||||||
insert_semi: false,
|
|
||||||
pending_line_info: None,
|
|
||||||
};
|
|
||||||
s.next(); // read the first character
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)] // Allow complex scan function
|
|
||||||
pub fn scan(&mut self) -> Result<Step<'a>> {
|
|
||||||
let insert_semi = self.insert_semi;
|
|
||||||
self.insert_semi = false;
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
self.reset_start();
|
|
||||||
|
|
||||||
match c {
|
|
||||||
' ' | '\t' | '\r' => {
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
'\n' => {
|
|
||||||
self.next();
|
|
||||||
if insert_semi {
|
|
||||||
return Ok((self.position(), Token::SEMICOLON, "\n"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(c) = self.current_char {
|
|
||||||
match c {
|
|
||||||
'+' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::ADD_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
Some('+') => {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::INC, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::ADD, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'-' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::SUB_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
Some('-') => {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::DEC, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::SUB, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'*' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::MUL_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::MUL, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'/' => match self.peek() {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::QUO_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
Some('/') => {
|
|
||||||
if insert_semi {
|
|
||||||
return Ok((self.position(), Token::SEMICOLON, "\n"));
|
|
||||||
}
|
|
||||||
return self.scan_line_comment();
|
|
||||||
}
|
|
||||||
Some('*') => {
|
|
||||||
if insert_semi && self.find_line_end() {
|
|
||||||
return Ok((self.position(), Token::SEMICOLON, "\n"));
|
|
||||||
}
|
|
||||||
return self.scan_general_comment();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::QUO, ""));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'%' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::REM_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::REM, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'&' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::AND_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
Some('&') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::LAND, ""));
|
|
||||||
}
|
|
||||||
Some('^') => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::AND_NOT_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::AND_NOT, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::AND, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'|' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::OR_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
Some('|') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::LOR, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::OR, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'^' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::XOR_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::XOR, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'<' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('<') => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::SHL_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::SHL, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::LEQ, ""));
|
|
||||||
}
|
|
||||||
Some('-') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::ARROW, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::LSS, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'>' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('>') => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::SHR_ASSIGN, ""));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Ok((self.position(), Token::SHR, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::GEQ, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::GTR, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
':' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::DEFINE, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::COLON, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'!' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::NEQ, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::NOT, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
',' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::COMMA, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
'(' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::LPAREN, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
')' => {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::RPAREN, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
'[' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::LBRACK, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
']' => {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::RBRACK, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
'{' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::LBRACE, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
'}' => {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::RBRACE, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
';' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::SEMICOLON, ";"));
|
|
||||||
}
|
|
||||||
|
|
||||||
'.' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('0'..='9') => return self.scan_int_or_float_or_imag(true),
|
|
||||||
Some('.') => match self.peek() {
|
|
||||||
Some('.') => {
|
|
||||||
self.next();
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::ELLIPSIS, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::PERIOD, "")),
|
|
||||||
},
|
|
||||||
_ => return Ok((self.position(), Token::PERIOD, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'=' => {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('=') => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::EQL, ""));
|
|
||||||
}
|
|
||||||
_ => return Ok((self.position(), Token::ASSIGN, "")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'0'..='9' => return self.scan_int_or_float_or_imag(false),
|
|
||||||
'\'' => return self.scan_rune(),
|
|
||||||
'"' => return self.scan_interpreted_string(),
|
|
||||||
'`' => return self.scan_raw_string(),
|
|
||||||
_ => return self.scan_pkg_or_keyword_or_ident(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reset_start();
|
|
||||||
if insert_semi {
|
|
||||||
Ok((self.position(), Token::SEMICOLON, "\n"))
|
|
||||||
} else {
|
|
||||||
Ok((self.position(), Token::EOF, ""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Keywords
|
|
||||||
// https://golang.org/ref/spec#Identifiers
|
|
||||||
fn scan_pkg_or_keyword_or_ident(&mut self) -> Result<Step<'a>> {
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
if !(is_letter(c) || is_unicode_digit(c)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos = self.position();
|
|
||||||
let literal = self.literal();
|
|
||||||
|
|
||||||
if literal.len() > 1 {
|
|
||||||
if let Some(&token) = KEYWORDS.get(literal) {
|
|
||||||
self.insert_semi = matches!(
|
|
||||||
token,
|
|
||||||
Token::BREAK | Token::CONTINUE | Token::FALLTHROUGH | Token::RETURN
|
|
||||||
);
|
|
||||||
return Ok((pos, token, literal));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.insert_semi = true;
|
|
||||||
Ok((pos, Token::IDENT, literal))
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Integer_literals
|
|
||||||
// https://golang.org/ref/spec#Floating-point_literals
|
|
||||||
// https://golang.org/ref/spec#Imaginary_literals
|
|
||||||
fn scan_int_or_float_or_imag(&mut self, preceding_dot: bool) -> Result<Step<'a>> {
|
|
||||||
self.insert_semi = true;
|
|
||||||
|
|
||||||
let mut token = Token::INT;
|
|
||||||
let mut digits = "_0123456789";
|
|
||||||
let mut exp = "eE";
|
|
||||||
|
|
||||||
if !preceding_dot {
|
|
||||||
if matches!(self.current_char, Some('0')) {
|
|
||||||
self.next();
|
|
||||||
match self.current_char {
|
|
||||||
Some('b' | 'B') => {
|
|
||||||
digits = "_01";
|
|
||||||
exp = "";
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
Some('o' | 'O') => {
|
|
||||||
digits = "_01234567";
|
|
||||||
exp = "";
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
Some('x' | 'X') => {
|
|
||||||
digits = "_0123456789abcdefABCDEF";
|
|
||||||
exp = "pP";
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
if !digits.contains(c) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if preceding_dot || matches!(self.current_char, Some('.')) {
|
|
||||||
token = Token::FLOAT;
|
|
||||||
self.next();
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
if !digits.contains(c) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exp.is_empty() {
|
|
||||||
if let Some(c) = self.current_char {
|
|
||||||
if exp.contains(c) {
|
|
||||||
token = Token::FLOAT;
|
|
||||||
self.next();
|
|
||||||
if matches!(self.current_char, Some('-' | '+')) {
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
if !matches!(c, '_' | '0'..='9') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(self.current_char, Some('i')) {
|
|
||||||
token = Token::IMAG;
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((self.position(), token, self.literal()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Rune_literals
|
|
||||||
fn scan_rune(&mut self) -> Result<Step<'a>> {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
match self.current_char {
|
|
||||||
Some('\\') => self.require_escaped_char::<'\''>()?,
|
|
||||||
Some(_) => self.next(),
|
|
||||||
_ => return Err(ScannerError::UnterminatedRune),
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(self.current_char, Some('\'')) {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::CHAR, self.literal()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ScannerError::UnterminatedRune)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#String_literals
|
|
||||||
fn scan_interpreted_string(&mut self) -> Result<Step<'a>> {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
match c {
|
|
||||||
'"' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::STRING, self.literal()));
|
|
||||||
}
|
|
||||||
'\\' => self.require_escaped_char::<'"'>()?,
|
|
||||||
_ => self.next(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ScannerError::UnterminatedString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#String_literals
|
|
||||||
fn scan_raw_string(&mut self) -> Result<Step<'a>> {
|
|
||||||
self.insert_semi = true;
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
match c {
|
|
||||||
'`' => {
|
|
||||||
self.next();
|
|
||||||
return Ok((self.position(), Token::STRING, self.literal()));
|
|
||||||
}
|
|
||||||
_ => self.next(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ScannerError::UnterminatedString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Comments
|
|
||||||
fn scan_general_comment(&mut self) -> Result<Step<'a>> {
|
|
||||||
self.next();
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
match c {
|
|
||||||
'*' => {
|
|
||||||
self.next();
|
|
||||||
if matches!(self.current_char, Some('/')) {
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
let pos = self.position();
|
|
||||||
let lit = self.literal();
|
|
||||||
|
|
||||||
// look for compiler directives
|
|
||||||
self.directive(&lit["/*".len()..lit.len() - "*/".len()], true)?;
|
|
||||||
|
|
||||||
return Ok((pos, Token::COMMENT, lit));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => self.next(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ScannerError::UnterminatedComment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Comments
|
|
||||||
fn scan_line_comment(&mut self) -> Result<Step<'a>> {
|
|
||||||
self.next();
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
while let Some(c) = self.current_char {
|
|
||||||
if is_newline(c) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos = self.position();
|
|
||||||
let lit = self.literal();
|
|
||||||
|
|
||||||
// look for compiler directives (at the beginning of line)
|
|
||||||
if self.start_column == 1 {
|
|
||||||
self.directive(lit["//".len()..].trim_end(), false)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((pos, Token::COMMENT, self.literal()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
|
|
||||||
fn directive(&mut self, input: &'a str, immediate: bool) -> Result<()> {
|
|
||||||
if let Some(line_directive) = input.strip_prefix("line ") {
|
|
||||||
self.pending_line_info = self.parse_line_directive(line_directive)?;
|
|
||||||
if immediate {
|
|
||||||
self.consume_pending_line_info();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_line_directive(&mut self, line_directive: &'a str) -> Result<Option<LineInfo<'a>>> {
|
|
||||||
if let Some((file, line)) = line_directive.rsplit_once(':') {
|
|
||||||
let line = line.parse().map_err(|_| ScannerError::InvalidDirective)?;
|
|
||||||
|
|
||||||
if let Some((file, l)) = file.rsplit_once(':') {
|
|
||||||
if let Ok(l) = l.parse() {
|
|
||||||
//line :line:col
|
|
||||||
//line filename:line:col
|
|
||||||
/*line :line:col*/
|
|
||||||
/*line filename:line:col*/
|
|
||||||
let file = if !file.is_empty() { Some(file) } else { None };
|
|
||||||
let col = Some(line);
|
|
||||||
let line = l;
|
|
||||||
let hide_column = false;
|
|
||||||
return Ok(Some((file, line, col, hide_column)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//line :line
|
|
||||||
//line filename:line
|
|
||||||
/*line :line*/
|
|
||||||
/*line filename:line*/
|
|
||||||
Ok(Some((Some(file), line, None, true)))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn find_line_end(&self) -> bool {
|
|
||||||
let buffer = self.buffer.as_bytes();
|
|
||||||
let mut in_comment = true;
|
|
||||||
|
|
||||||
let mut i = self.offset;
|
|
||||||
let max = self.buffer.len();
|
|
||||||
while i < max {
|
|
||||||
let c = buffer[i] as char;
|
|
||||||
|
|
||||||
if i < max - 1 {
|
|
||||||
let n = buffer[i + 1] as char;
|
|
||||||
|
|
||||||
if !in_comment && c == '/' && n == '/' {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == '/' && n == '*' {
|
|
||||||
i += 2;
|
|
||||||
in_comment = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == '*' && n == '/' {
|
|
||||||
i += 2;
|
|
||||||
in_comment = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_newline(c) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !in_comment && !matches!(c, ' ' | '\t' | '\r') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
!in_comment
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_pending_line_info(&mut self) {
|
|
||||||
if let Some(line_info) = self.pending_line_info.take() {
|
|
||||||
if let Some(file) = line_info.0 {
|
|
||||||
self.file = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.line = line_info.1;
|
|
||||||
|
|
||||||
if let Some(column) = line_info.2 {
|
|
||||||
self.column = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.hide_column = line_info.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn peek(&mut self) -> Option<char> {
|
|
||||||
self.chars.peek().copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next(&mut self) {
|
|
||||||
self.offset += self.current_char_len;
|
|
||||||
self.column += self.current_char_len;
|
|
||||||
let last_char = self.current_char;
|
|
||||||
|
|
||||||
self.current_char = self.chars.next();
|
|
||||||
if let Some(c) = self.current_char {
|
|
||||||
self.current_char_len = c.len_utf8();
|
|
||||||
if matches!(last_char, Some('\n')) {
|
|
||||||
self.line += 1;
|
|
||||||
self.column = 1;
|
|
||||||
self.consume_pending_line_info();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.current_char_len = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn position(&self) -> Position<'a> {
|
|
||||||
Position {
|
|
||||||
directory: self.directory,
|
|
||||||
file: self.file,
|
|
||||||
offset: self.start_offset,
|
|
||||||
line: self.start_line,
|
|
||||||
column: if self.hide_column {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
self.start_column
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_start(&mut self) {
|
|
||||||
self.start_offset = self.offset;
|
|
||||||
self.start_line = self.line;
|
|
||||||
self.start_column = self.column;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn literal(&self) -> &'a str {
|
|
||||||
&self.buffer[self.start_offset..self.offset]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_escaped_char<const DELIM: char>(&mut self) -> Result<()> {
|
|
||||||
self.next();
|
|
||||||
|
|
||||||
let c = self
|
|
||||||
.current_char
|
|
||||||
.ok_or(ScannerError::UnterminatedEscapedChar)?;
|
|
||||||
|
|
||||||
// TODO: move this to the match when const generics can be referenced in patterns
|
|
||||||
if c == DELIM {
|
|
||||||
self.next();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
match c {
|
|
||||||
'a' | 'b' | 'f' | 'n' | 'r' | 't' | 'v' | '\\' => self.next(),
|
|
||||||
'x' => {
|
|
||||||
self.next();
|
|
||||||
self.require_hex_digits::<2>()?
|
|
||||||
}
|
|
||||||
'u' => {
|
|
||||||
self.next();
|
|
||||||
self.require_hex_digits::<4>()?;
|
|
||||||
}
|
|
||||||
'U' => {
|
|
||||||
self.next();
|
|
||||||
self.require_hex_digits::<8>()?;
|
|
||||||
}
|
|
||||||
'0'..='7' => self.require_octal_digits::<3>()?,
|
|
||||||
_ => return Err(ScannerError::UnterminatedEscapedChar),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_octal_digits<const COUNT: usize>(&mut self) -> Result<()> {
|
|
||||||
for _ in 0..COUNT {
|
|
||||||
let c = self.current_char.ok_or(ScannerError::OctalNotFound)?;
|
|
||||||
|
|
||||||
if !is_octal_digit(c) {
|
|
||||||
return Err(ScannerError::OctalNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_hex_digits<const COUNT: usize>(&mut self) -> Result<()> {
|
|
||||||
for _ in 0..COUNT {
|
|
||||||
let c = self.current_char.ok_or(ScannerError::HexadecimalNotFound)?;
|
|
||||||
|
|
||||||
if !is_hex_digit(c) {
|
|
||||||
return Err(ScannerError::HexadecimalNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIterator for Scanner<'a> {
|
|
||||||
type Item = Result<Step<'a>>;
|
|
||||||
type IntoIter = IntoIter<'a>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
Self::IntoIter::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IntoIter<'a> {
|
|
||||||
scanner: Scanner<'a>,
|
|
||||||
done: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIter<'a> {
|
|
||||||
const fn new(scanner: Scanner<'a>) -> Self {
|
|
||||||
Self { scanner, done: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for IntoIter<'a> {
|
|
||||||
type Item = Result<Step<'a>>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.done {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.scanner.scan() {
|
|
||||||
Ok((pos, tok, lit)) => {
|
|
||||||
if tok == Token::EOF {
|
|
||||||
self.done = true;
|
|
||||||
}
|
|
||||||
Some(Ok((pos, tok, lit)))
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.done = true;
|
|
||||||
Some(Err(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Letters_and_digits
|
|
||||||
|
|
||||||
fn is_letter(c: char) -> bool {
|
|
||||||
c == '_' || is_unicode_letter(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
//const fn is_decimal_digit(c: char) -> bool {
|
|
||||||
//matches!(c, '0'..='9')
|
|
||||||
//}
|
|
||||||
|
|
||||||
//const fn is_binary_digit(c: char) -> bool {
|
|
||||||
//matches!(c, '0'..='1')
|
|
||||||
//}
|
|
||||||
|
|
||||||
const fn is_octal_digit(c: char) -> bool {
|
|
||||||
matches!(c, '0'..='7')
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn is_hex_digit(c: char) -> bool {
|
|
||||||
matches!(c, '0'..='9' | 'A'..='F' | 'a'..='f')
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Characters
|
|
||||||
|
|
||||||
const fn is_newline(c: char) -> bool {
|
|
||||||
c == '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
//const fn is_unicode_char(c: char) -> bool {
|
|
||||||
//c != '\n'
|
|
||||||
//}
|
|
||||||
|
|
||||||
fn is_unicode_letter(c: char) -> bool {
|
|
||||||
matches!(
|
|
||||||
get_general_category(c),
|
|
||||||
GeneralCategory::UppercaseLetter
|
|
||||||
| GeneralCategory::LowercaseLetter
|
|
||||||
| GeneralCategory::TitlecaseLetter
|
|
||||||
| GeneralCategory::ModifierLetter
|
|
||||||
| GeneralCategory::OtherLetter
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_unicode_digit(c: char) -> bool {
|
|
||||||
get_general_category(c) == GeneralCategory::DecimalNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://golang.org/ref/spec#Keywords
|
|
||||||
|
|
||||||
static KEYWORDS: Map<&'static str, Token> = phf_map! {
|
|
||||||
"break" => Token::BREAK,
|
|
||||||
"case" => Token::CASE,
|
|
||||||
"chan" => Token::CHAN,
|
|
||||||
"const" => Token::CONST,
|
|
||||||
"continue" => Token::CONTINUE,
|
|
||||||
|
|
||||||
"default" => Token::DEFAULT,
|
|
||||||
"defer" => Token::DEFER,
|
|
||||||
"else" => Token::ELSE,
|
|
||||||
"fallthrough" => Token::FALLTHROUGH,
|
|
||||||
"for" => Token::FOR,
|
|
||||||
|
|
||||||
"func" => Token::FUNC,
|
|
||||||
"go" => Token::GO,
|
|
||||||
"goto" => Token::GOTO,
|
|
||||||
"if" => Token::IF,
|
|
||||||
"import" => Token::IMPORT,
|
|
||||||
|
|
||||||
"interface" => Token::INTERFACE,
|
|
||||||
"map" => Token::MAP,
|
|
||||||
"package" => Token::PACKAGE,
|
|
||||||
"range" => Token::RANGE,
|
|
||||||
"return" => Token::RETURN,
|
|
||||||
|
|
||||||
"select" => Token::SELECT,
|
|
||||||
"struct" => Token::STRUCT,
|
|
||||||
"switch" => Token::SWITCH,
|
|
||||||
"type" => Token::TYPE,
|
|
||||||
"var" => Token::VAR,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::Scanner;
|
|
||||||
|
|
||||||
#[test] // fuzz
|
|
||||||
fn it_should_return_an_error_on_missing_line_number() {
|
|
||||||
let input = "/*line :*/";
|
|
||||||
let mut out: Vec<_> = Scanner::new(file!(), input).into_iter().collect();
|
|
||||||
assert!(out.pop().unwrap().is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/go/token/token.go
|
|
||||||
|
|
||||||
#![allow(non_camel_case_types)] // For consistency with the Go tokens
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct Position<'a> {
|
|
||||||
pub directory: &'a str,
|
|
||||||
pub file: &'a str,
|
|
||||||
pub offset: usize,
|
|
||||||
pub line: usize,
|
|
||||||
pub column: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> fmt::Display for Position<'a> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
if self.file.is_empty() {
|
|
||||||
write!(f, ":{}:{}", self.line, self.column)
|
|
||||||
} else if self.file.starts_with('/') {
|
|
||||||
write!(f, "{}:{}:{}", self.file, self.line, self.column)
|
|
||||||
} else {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}/{}:{}:{}",
|
|
||||||
self.directory, self.file, self.line, self.column
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub enum Token {
|
|
||||||
EOF,
|
|
||||||
COMMENT,
|
|
||||||
|
|
||||||
IDENT, // main
|
|
||||||
INT, // 12345
|
|
||||||
FLOAT, // 123.45
|
|
||||||
IMAG, // 123.45i
|
|
||||||
CHAR, // 'a'
|
|
||||||
STRING, // "abc"
|
|
||||||
|
|
||||||
ADD, // +
|
|
||||||
SUB, // -
|
|
||||||
MUL, // *
|
|
||||||
QUO, // /
|
|
||||||
REM, // %
|
|
||||||
|
|
||||||
AND, // &
|
|
||||||
OR, // |
|
|
||||||
XOR, // ^
|
|
||||||
SHL, // <<
|
|
||||||
SHR, // >>
|
|
||||||
AND_NOT, // &^
|
|
||||||
|
|
||||||
ADD_ASSIGN, // +=
|
|
||||||
SUB_ASSIGN, // -=
|
|
||||||
MUL_ASSIGN, // *=
|
|
||||||
QUO_ASSIGN, // /=
|
|
||||||
REM_ASSIGN, // %=
|
|
||||||
|
|
||||||
AND_ASSIGN, // &=
|
|
||||||
OR_ASSIGN, // |=
|
|
||||||
XOR_ASSIGN, // ^=
|
|
||||||
SHL_ASSIGN, // <<=
|
|
||||||
SHR_ASSIGN, // >>=
|
|
||||||
AND_NOT_ASSIGN, // &^=
|
|
||||||
|
|
||||||
LAND, // &&
|
|
||||||
LOR, // ||
|
|
||||||
ARROW, // <-
|
|
||||||
INC, // ++
|
|
||||||
DEC, // --
|
|
||||||
|
|
||||||
EQL, // ==
|
|
||||||
LSS, // <
|
|
||||||
GTR, // >
|
|
||||||
ASSIGN, // =
|
|
||||||
NOT, // !
|
|
||||||
|
|
||||||
NEQ, // !=
|
|
||||||
LEQ, // <=
|
|
||||||
GEQ, // >=
|
|
||||||
DEFINE, // :=
|
|
||||||
ELLIPSIS, // ...
|
|
||||||
|
|
||||||
LPAREN, // (
|
|
||||||
LBRACK, // [
|
|
||||||
LBRACE, // {
|
|
||||||
COMMA, // ,
|
|
||||||
PERIOD, // .
|
|
||||||
|
|
||||||
RPAREN, // )
|
|
||||||
RBRACK, // ]
|
|
||||||
RBRACE, // }
|
|
||||||
SEMICOLON, // ;
|
|
||||||
COLON, // :
|
|
||||||
|
|
||||||
BREAK,
|
|
||||||
CASE,
|
|
||||||
CHAN,
|
|
||||||
CONST,
|
|
||||||
CONTINUE,
|
|
||||||
|
|
||||||
DEFAULT,
|
|
||||||
DEFER,
|
|
||||||
ELSE,
|
|
||||||
FALLTHROUGH,
|
|
||||||
FOR,
|
|
||||||
|
|
||||||
FUNC,
|
|
||||||
GO,
|
|
||||||
GOTO,
|
|
||||||
IF,
|
|
||||||
IMPORT,
|
|
||||||
|
|
||||||
INTERFACE,
|
|
||||||
MAP,
|
|
||||||
PACKAGE,
|
|
||||||
RANGE,
|
|
||||||
RETURN,
|
|
||||||
|
|
||||||
SELECT,
|
|
||||||
STRUCT,
|
|
||||||
SWITCH,
|
|
||||||
TYPE,
|
|
||||||
VAR,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Token {
|
|
||||||
pub const fn is_assign_op(&self) -> bool {
|
|
||||||
use Token::*;
|
|
||||||
matches!(
|
|
||||||
self,
|
|
||||||
ADD_ASSIGN
|
|
||||||
| SUB_ASSIGN
|
|
||||||
| MUL_ASSIGN
|
|
||||||
| QUO_ASSIGN
|
|
||||||
| REM_ASSIGN
|
|
||||||
| AND_ASSIGN
|
|
||||||
| OR_ASSIGN
|
|
||||||
| XOR_ASSIGN
|
|
||||||
| SHL_ASSIGN
|
|
||||||
| SHR_ASSIGN
|
|
||||||
| AND_NOT_ASSIGN
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://go.dev/ref/spec#Operator_precedence
|
|
||||||
pub fn precedence(&self) -> u8 {
|
|
||||||
use Token::*;
|
|
||||||
match self {
|
|
||||||
MUL | QUO | REM | SHL | SHR | AND | AND_NOT => 5,
|
|
||||||
ADD | SUB | OR | XOR => 4,
|
|
||||||
EQL | NEQ | LSS | LEQ | GTR | GEQ => 3,
|
|
||||||
LAND => 2,
|
|
||||||
LOR => 1,
|
|
||||||
_ => unreachable!(
|
|
||||||
"precedence() is only supported for binary operators, called with: {:?}",
|
|
||||||
self
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn lowest_precedence() -> u8 {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Token> for &'static str {
|
|
||||||
fn from(token: &Token) -> Self {
|
|
||||||
use Token::*;
|
|
||||||
|
|
||||||
match token {
|
|
||||||
EOF => "EOF",
|
|
||||||
COMMENT => "COMMENT",
|
|
||||||
|
|
||||||
IDENT => "IDENT",
|
|
||||||
INT => "INT",
|
|
||||||
FLOAT => "FLOAT",
|
|
||||||
IMAG => "IMAG",
|
|
||||||
CHAR => "CHAR",
|
|
||||||
STRING => "STRING",
|
|
||||||
|
|
||||||
ADD => "+",
|
|
||||||
SUB => "-",
|
|
||||||
MUL => "*",
|
|
||||||
QUO => "/",
|
|
||||||
REM => "%",
|
|
||||||
|
|
||||||
AND => "&",
|
|
||||||
OR => "|",
|
|
||||||
XOR => "^",
|
|
||||||
SHL => "<<",
|
|
||||||
SHR => ">>",
|
|
||||||
AND_NOT => "&^",
|
|
||||||
|
|
||||||
ADD_ASSIGN => "+=",
|
|
||||||
SUB_ASSIGN => "-=",
|
|
||||||
MUL_ASSIGN => "*=",
|
|
||||||
QUO_ASSIGN => "/=",
|
|
||||||
REM_ASSIGN => "%=",
|
|
||||||
|
|
||||||
AND_ASSIGN => "&=",
|
|
||||||
OR_ASSIGN => "|=",
|
|
||||||
XOR_ASSIGN => "^=",
|
|
||||||
SHL_ASSIGN => "<<=",
|
|
||||||
SHR_ASSIGN => ">>=",
|
|
||||||
AND_NOT_ASSIGN => "&^=",
|
|
||||||
|
|
||||||
LAND => "&&",
|
|
||||||
LOR => "||",
|
|
||||||
ARROW => "<-",
|
|
||||||
INC => "++",
|
|
||||||
DEC => "--",
|
|
||||||
|
|
||||||
EQL => "==",
|
|
||||||
LSS => "<",
|
|
||||||
GTR => ">",
|
|
||||||
ASSIGN => "=",
|
|
||||||
NOT => "!",
|
|
||||||
|
|
||||||
NEQ => "!=",
|
|
||||||
LEQ => "<=",
|
|
||||||
GEQ => ">=",
|
|
||||||
DEFINE => ":=",
|
|
||||||
ELLIPSIS => "...",
|
|
||||||
|
|
||||||
LPAREN => "(",
|
|
||||||
LBRACK => "[",
|
|
||||||
LBRACE => "{",
|
|
||||||
COMMA => ",",
|
|
||||||
PERIOD => ".",
|
|
||||||
|
|
||||||
RPAREN => ")",
|
|
||||||
RBRACK => "]",
|
|
||||||
RBRACE => "}",
|
|
||||||
SEMICOLON => ";",
|
|
||||||
COLON => ":",
|
|
||||||
|
|
||||||
BREAK => "break",
|
|
||||||
CASE => "case",
|
|
||||||
CHAN => "chan",
|
|
||||||
CONST => "const",
|
|
||||||
CONTINUE => "continue",
|
|
||||||
|
|
||||||
DEFAULT => "default",
|
|
||||||
DEFER => "defer",
|
|
||||||
ELSE => "else",
|
|
||||||
FALLTHROUGH => "fallthrough",
|
|
||||||
FOR => "for",
|
|
||||||
|
|
||||||
FUNC => "func",
|
|
||||||
GO => "go",
|
|
||||||
GOTO => "goto",
|
|
||||||
IF => "if",
|
|
||||||
IMPORT => "import",
|
|
||||||
|
|
||||||
INTERFACE => "interface",
|
|
||||||
MAP => "map",
|
|
||||||
PACKAGE => "package",
|
|
||||||
RANGE => "range",
|
|
||||||
RETURN => "return",
|
|
||||||
|
|
||||||
SELECT => "select",
|
|
||||||
STRUCT => "struct",
|
|
||||||
SWITCH => "switch",
|
|
||||||
TYPE => "type",
|
|
||||||
VAR => "var",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -181,6 +181,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
|||||||
"git" => "GitPython",
|
"git" => "GitPython",
|
||||||
"u" => "requests",
|
"u" => "requests",
|
||||||
"f" => "requests",
|
"f" => "requests",
|
||||||
|
"." => "requests",
|
||||||
"shopify" => "ShopifyAPI",
|
"shopify" => "ShopifyAPI",
|
||||||
"seleniumwire" => "selenium-wire",
|
"seleniumwire" => "selenium-wire",
|
||||||
"openbb-terminal" => "openbb[all]",
|
"openbb-terminal" => "openbb[all]",
|
||||||
@@ -217,20 +218,29 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
|
|||||||
let ast = parser::parse_program(code, "main.py").map_err(|e| {
|
let ast = parser::parse_program(code, "main.py").map_err(|e| {
|
||||||
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
|
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
|
||||||
})?;
|
})?;
|
||||||
let imports = ast
|
let mut imports: Vec<String> = ast
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|x| match x {
|
.filter_map(|x| match x {
|
||||||
Located { node, .. } => match node {
|
Located { node, .. } => match node {
|
||||||
StmtKind::Import { names } => Some(
|
StmtKind::Import { names } => Some(
|
||||||
names
|
names
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| x.node.name.split('.').next().unwrap_or("").to_string())
|
.map(|x| {
|
||||||
|
let name = x.node.name;
|
||||||
|
if name.starts_with('.') {
|
||||||
|
".".to_string()
|
||||||
|
} else {
|
||||||
|
name.split('.').next().unwrap_or("").to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(replace_import)
|
.map(replace_import)
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
),
|
),
|
||||||
|
StmtKind::ImportFrom { level: Some(i), .. } if i > 0 => {
|
||||||
|
Some(vec!["requests".to_string()])
|
||||||
|
}
|
||||||
StmtKind::ImportFrom { level: _, module: Some(mod_), names: _ } => {
|
StmtKind::ImportFrom { level: _, module: Some(mod_), names: _ } => {
|
||||||
let imprt = mod_.split('.').next().unwrap_or("").replace("_", "-");
|
let imprt = mod_.split('.').next().unwrap_or("").replace("_", "-");
|
||||||
|
|
||||||
Some(vec![replace_import(imprt)])
|
Some(vec![replace_import(imprt)])
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -240,7 +250,7 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
|
|||||||
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
|
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
|
||||||
.unique()
|
.unique()
|
||||||
.collect();
|
.collect();
|
||||||
|
imports.sort();
|
||||||
Ok(imports)
|
Ok(imports)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,6 +452,7 @@ import os
|
|||||||
import wmill
|
import wmill
|
||||||
from zanzibar.estonie import talin
|
from zanzibar.estonie import talin
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from . import tests
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
pass
|
pass
|
||||||
@@ -449,7 +460,7 @@ def main():
|
|||||||
";
|
";
|
||||||
let r = parse_python_imports(code)?;
|
let r = parse_python_imports(code)?;
|
||||||
// println!("{}", serde_json::to_string(&r)?);
|
// println!("{}", serde_json::to_string(&r)?);
|
||||||
assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib"]);
|
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,22 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
|
use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
|
||||||
|
use serde_json::Value;
|
||||||
use windmill_common::error;
|
use windmill_common::error;
|
||||||
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ};
|
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ};
|
||||||
|
|
||||||
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Spanned};
|
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
|
||||||
use swc_ecma_ast::{
|
use swc_ecma_ast::{
|
||||||
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit,
|
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit,
|
||||||
ModuleDecl, ModuleItem, Number, ObjectLit, Pat, Str, TsArrayType, TsEntityName, TsKeywordType,
|
ModuleDecl, ModuleItem, Number, ObjectLit, Param, Pat, Str, TsArrayType, TsEntityName,
|
||||||
TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature, TsType,
|
TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature,
|
||||||
TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
|
TsType, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
|
||||||
};
|
};
|
||||||
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
|
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
|
||||||
|
|
||||||
pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
|
pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> error::Result<MainArgSignature> {
|
||||||
let cm: Lrc<SourceMap> = Default::default();
|
let cm: Lrc<SourceMap> = Default::default();
|
||||||
let fm = cm.new_source_file(FileName::Custom("test.ts".into()), code.into());
|
let fm = cm.new_source_file(FileName::Custom("main.ts".into()), code.into());
|
||||||
let lexer = Lexer::new(
|
let lexer = Lexer::new(
|
||||||
// We want to parse ecmascript
|
// We want to parse ecmascript
|
||||||
Syntax::Typescript(TsConfig::default()),
|
Syntax::Typescript(TsConfig::default()),
|
||||||
@@ -54,65 +55,17 @@ pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
|
|||||||
})) if &sym.to_string() == "main" => Some(function.params),
|
})) if &sym.to_string() == "main" => Some(function.params),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(params) = params {
|
if let Some(params) = params {
|
||||||
Ok(MainArgSignature {
|
let r = MainArgSignature {
|
||||||
star_args: false,
|
star_args: false,
|
||||||
star_kwargs: false,
|
star_kwargs: false,
|
||||||
args: params
|
args: params
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| match x.pat {
|
.map(|x| parse_param(x, &cm, skip_dflt))
|
||||||
Pat::Ident(ident) => {
|
|
||||||
let (name, typ, nullable) = binding_ident_to_arg(&ident);
|
|
||||||
Ok(Arg {
|
|
||||||
otyp: None,
|
|
||||||
name,
|
|
||||||
typ,
|
|
||||||
default: None,
|
|
||||||
has_default: ident.id.optional || nullable,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Pat::Assign(AssignPat { left, right, .. }) => {
|
|
||||||
let (name, mut typ, _nullable) =
|
|
||||||
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
|
|
||||||
error::Error::ExecutionErr(format!(
|
|
||||||
"parameter syntax unsupported: `{}`",
|
|
||||||
cm.span_to_snippet(left.span())
|
|
||||||
.unwrap_or_else(|_| cm.span_to_string(left.span()))
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let span = match *right {
|
|
||||||
Expr::Lit(Lit::Str(Str { span, .. })) => Some(span),
|
|
||||||
Expr::Lit(Lit::Num(Number { span, .. })) => Some(span),
|
|
||||||
Expr::Lit(Lit::BigInt(BigInt { span, .. })) => Some(span),
|
|
||||||
Expr::Lit(Lit::Bool(Bool { span, .. })) => Some(span),
|
|
||||||
Expr::Object(ObjectLit { span, .. }) => Some(span),
|
|
||||||
Expr::Array(ArrayLit { span, .. }) => Some(span),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let expr = span
|
|
||||||
.and_then(|x| cm.span_to_snippet(x).ok())
|
|
||||||
.map(|x| serde_json::from_str(&x).map_err(|_| x));
|
|
||||||
|
|
||||||
let default = match expr.clone() {
|
|
||||||
Some(Ok(x)) => Some(x),
|
|
||||||
Some(Err(x)) => eval_sync(&x).ok(),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if typ == Typ::Unknown && default.is_some() {
|
|
||||||
typ = json_to_typ(default.as_ref().unwrap());
|
|
||||||
}
|
|
||||||
Ok(Arg { otyp: None, name, typ, default, has_default: true })
|
|
||||||
}
|
|
||||||
_ => Err(error::Error::ExecutionErr(format!(
|
|
||||||
"parameter syntax unsupported: `{}`",
|
|
||||||
cm.span_to_snippet(x.span())
|
|
||||||
.unwrap_or_else(|_| cm.span_to_string(x.span()))
|
|
||||||
))),
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<Arg>, error::Error>>()?,
|
.collect::<Result<Vec<Arg>, error::Error>>()?,
|
||||||
})
|
};
|
||||||
|
Ok(r)
|
||||||
} else {
|
} else {
|
||||||
Err(error::Error::ExecutionErr(
|
Err(error::Error::ExecutionErr(
|
||||||
"main function was not findable (expected to find 'export function main(...)'"
|
"main function was not findable (expected to find 'export function main(...)'"
|
||||||
@@ -121,6 +74,75 @@ pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_param(x: Param, cm: &Lrc<SourceMap>, skip_dflt: bool) -> error::Result<Arg> {
|
||||||
|
let r = match x.pat {
|
||||||
|
Pat::Ident(ident) => {
|
||||||
|
let (name, typ, nullable) = binding_ident_to_arg(&ident);
|
||||||
|
Ok(Arg {
|
||||||
|
otyp: None,
|
||||||
|
name,
|
||||||
|
typ,
|
||||||
|
default: None,
|
||||||
|
has_default: ident.id.optional || nullable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Pat::Assign(AssignPat { left, right, .. }) => {
|
||||||
|
let (name, mut typ, _nullable) =
|
||||||
|
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
|
||||||
|
error::Error::ExecutionErr(format!(
|
||||||
|
"parameter syntax unsupported: `{}`",
|
||||||
|
cm.span_to_snippet(left.span())
|
||||||
|
.unwrap_or_else(|_| cm.span_to_string(left.span()))
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let dflt = if skip_dflt {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match *right {
|
||||||
|
Expr::Lit(Lit::Str(Str { value, .. })) => {
|
||||||
|
Some(Value::String(value.to_string()))
|
||||||
|
}
|
||||||
|
Expr::Lit(Lit::Num(Number { value, .. }))
|
||||||
|
if (value == (value as u64) as f64) =>
|
||||||
|
{
|
||||||
|
Some(serde_json::json!(value as u64))
|
||||||
|
}
|
||||||
|
Expr::Lit(Lit::Num(Number { value, .. })) => Some(serde_json::json!(value)),
|
||||||
|
Expr::Lit(Lit::BigInt(BigInt { value, .. })) => Some(serde_json::json!(value)),
|
||||||
|
Expr::Lit(Lit::Bool(Bool { value, .. })) => Some(Value::Bool(value)),
|
||||||
|
Expr::Object(ObjectLit { span, .. }) => eval_span(span, cm),
|
||||||
|
Expr::Array(ArrayLit { span, .. }) => eval_span(span, cm),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if typ == Typ::Unknown && dflt.is_some() {
|
||||||
|
typ = json_to_typ(dflt.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true })
|
||||||
|
}
|
||||||
|
_ => Err(error::Error::ExecutionErr(format!(
|
||||||
|
"parameter syntax unsupported: `{}`",
|
||||||
|
cm.span_to_snippet(x.span())
|
||||||
|
.unwrap_or_else(|_| cm.span_to_string(x.span()))
|
||||||
|
))),
|
||||||
|
};
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
|
||||||
|
let expr = cm
|
||||||
|
.span_to_snippet(span)
|
||||||
|
.ok()
|
||||||
|
.map(|x| serde_json::from_str(&x).map_err(|_| x));
|
||||||
|
|
||||||
|
match expr {
|
||||||
|
Some(Ok(x)) => Some(x),
|
||||||
|
Some(Err(x)) => eval_sync(&x).ok(),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
|
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
|
||||||
let (typ, nullable) = type_ann
|
let (typ, nullable) = type_ann
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -246,7 +268,7 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
|||||||
pub fn eval_sync(code: &str) -> Result<serde_json::Value, String> {
|
pub fn eval_sync(code: &str) -> Result<serde_json::Value, String> {
|
||||||
let mut context = JsRuntime::new(RuntimeOptions::default());
|
let mut context = JsRuntime::new(RuntimeOptions::default());
|
||||||
let code = format!("let x = {}; x", code);
|
let code = format!("let x = {}; x", code);
|
||||||
let res = context.execute_script("<anon>", &code);
|
let res = context.execute_script("<anon>", code);
|
||||||
match res {
|
match res {
|
||||||
Ok(global) => {
|
Ok(global) => {
|
||||||
let scope = &mut context.handle_scope();
|
let scope = &mut context.handle_scope();
|
||||||
@@ -282,7 +304,7 @@ export function main(test1?: string, test2: string = \"burkina\",
|
|||||||
}
|
}
|
||||||
";
|
";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_deno_signature(code)?,
|
parse_deno_signature(code, false)?,
|
||||||
MainArgSignature {
|
MainArgSignature {
|
||||||
star_args: false,
|
star_args: false,
|
||||||
star_kwargs: false,
|
star_kwargs: false,
|
||||||
@@ -394,7 +416,7 @@ export function main(test2 = \"burkina\",
|
|||||||
}
|
}
|
||||||
";
|
";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_deno_signature(code)?,
|
parse_deno_signature(code, false)?,
|
||||||
MainArgSignature {
|
MainArgSignature {
|
||||||
star_args: false,
|
star_args: false,
|
||||||
star_kwargs: false,
|
star_kwargs: false,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
|||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
|
use base64::Engine;
|
||||||
|
#[cfg(feature = "enterprise")]
|
||||||
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
|
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
@@ -11,9 +13,9 @@ pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
|
|||||||
.expect("license_key can be splitted with a .");
|
.expect("license_key can be splitted with a .");
|
||||||
|
|
||||||
let pub_key = rsa::RsaPublicKey::from_public_key_der(
|
let pub_key = rsa::RsaPublicKey::from_public_key_der(
|
||||||
&base64::decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
|
&base64::engine::general_purpose::STANDARD.decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
|
||||||
let msg = base64::decode(splitted_lk.0)?;
|
let msg = base64::engine::general_purpose::STANDARD.decode(splitted_lk.0)?;
|
||||||
let signature = base64::decode(splitted_lk.1)?;
|
let signature = base64::engine::general_purpose::STANDARD.decode(splitted_lk.1)?;
|
||||||
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
|
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
|
||||||
.verify(&msg, &rsa::pss::Signature::from(signature))
|
.verify(&msg, &rsa::pss::Signature::from(signature))
|
||||||
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;
|
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|||||||
|
|
||||||
use git_version::git_version;
|
use git_version::git_version;
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
use windmill_common::utils::rd_string;
|
use windmill_common::{utils::rd_string, METRICS_ADDR};
|
||||||
|
|
||||||
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||||
const DEFAULT_NUM_WORKERS: usize = 3;
|
const DEFAULT_NUM_WORKERS: usize = 3;
|
||||||
@@ -30,19 +30,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.and_then(|x| x.parse::<i32>().ok())
|
.and_then(|x| x.parse::<i32>().ok())
|
||||||
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
|
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
|
||||||
|
|
||||||
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
|
let metrics_addr: Option<SocketAddr> = *METRICS_ADDR;
|
||||||
.ok()
|
|
||||||
.map(|s| {
|
|
||||||
s.parse::<bool>()
|
|
||||||
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
|
|
||||||
.or_else(|_| s.parse::<SocketAddr>().map(Some))
|
|
||||||
})
|
|
||||||
.transpose()?
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
|
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|x| x.parse().ok() )
|
.and_then(|x| x.parse().ok())
|
||||||
.unwrap_or(IpAddr::from(DEFAULT_SERVER_BIND_ADDR));
|
.unwrap_or(IpAddr::from(DEFAULT_SERVER_BIND_ADDR));
|
||||||
|
|
||||||
let port: u16 = std::env::var("PORT")
|
let port: u16 = std::env::var("PORT")
|
||||||
@@ -57,8 +49,40 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.and_then(|x| x.parse::<bool>().ok())
|
.and_then(|x| x.parse::<bool>().ok())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let rsmq_config = std::env::var("REDIS_URL").ok().map(|x| {
|
||||||
|
let url = x.parse::<url::Url>().unwrap();
|
||||||
|
let mut config = rsmq_async::RsmqOptions { ..Default::default() };
|
||||||
|
|
||||||
|
config.host = url.host_str().expect("redis host required").to_owned();
|
||||||
|
config.password = url.password().map(|s| s.to_owned());
|
||||||
|
config.db = url
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|mut segments| segments.next())
|
||||||
|
.and_then(|segment| segment.parse().ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
config.ns = url
|
||||||
|
.query_pairs()
|
||||||
|
.find(|s| s.0 == "rsmq_namespace")
|
||||||
|
.map(|s| s.1)
|
||||||
|
.unwrap_or(std::borrow::Cow::Borrowed("rsmq"))
|
||||||
|
.into_owned();
|
||||||
|
config.port = url.port().unwrap_or(6379).to_string();
|
||||||
|
|
||||||
|
config
|
||||||
|
});
|
||||||
|
|
||||||
let db = windmill_common::connect_db(server_mode).await?;
|
let db = windmill_common::connect_db(server_mode).await?;
|
||||||
|
|
||||||
|
let rsmq = if let Some(config) = rsmq_config {
|
||||||
|
let mut rsmq = rsmq_async::MultiplexedRsmq::new(config).await.unwrap();
|
||||||
|
|
||||||
|
let _ = rsmq_async::RsmqConnection::create_queue(&mut rsmq, "main_queue", None, None, None)
|
||||||
|
.await;
|
||||||
|
Some(rsmq)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if server_mode {
|
if server_mode {
|
||||||
windmill_api::migrate_db(&db).await?;
|
windmill_api::migrate_db(&db).await?;
|
||||||
}
|
}
|
||||||
@@ -91,6 +115,8 @@ Windmill Community Edition {GIT_VERSION}
|
|||||||
"BASE_URL",
|
"BASE_URL",
|
||||||
"BASE_INTERNAL_URL",
|
"BASE_INTERNAL_URL",
|
||||||
"TIMEOUT",
|
"TIMEOUT",
|
||||||
|
"ZOMBIE_JOB_TIMEOUT",
|
||||||
|
"RESTART_ZOMBIE_JOBS",
|
||||||
"SLEEP_QUEUE",
|
"SLEEP_QUEUE",
|
||||||
"MAX_LOG_SIZE",
|
"MAX_LOG_SIZE",
|
||||||
"SERVER_BIND_ADDR",
|
"SERVER_BIND_ADDR",
|
||||||
@@ -102,6 +128,8 @@ Windmill Community Edition {GIT_VERSION}
|
|||||||
"PYTHON_PATH",
|
"PYTHON_PATH",
|
||||||
"DENO_PATH",
|
"DENO_PATH",
|
||||||
"GO_PATH",
|
"GO_PATH",
|
||||||
|
"GOPRIVATE",
|
||||||
|
"NETRC",
|
||||||
"PIP_INDEX_URL",
|
"PIP_INDEX_URL",
|
||||||
"PIP_EXTRA_INDEX_URL",
|
"PIP_EXTRA_INDEX_URL",
|
||||||
"PIP_TRUSTED_HOST",
|
"PIP_TRUSTED_HOST",
|
||||||
@@ -112,21 +140,23 @@ Windmill Community Edition {GIT_VERSION}
|
|||||||
"QUEUE_LIMIT_WAIT_RESULT",
|
"QUEUE_LIMIT_WAIT_RESULT",
|
||||||
"DENO_AUTH_TOKENS",
|
"DENO_AUTH_TOKENS",
|
||||||
"DENO_FLAGS",
|
"DENO_FLAGS",
|
||||||
|
"NPM_CONFIG_REGISTRY",
|
||||||
"PIP_LOCAL_DEPENDENCIES",
|
"PIP_LOCAL_DEPENDENCIES",
|
||||||
"ADDITIONAL_PYTHON_PATHS",
|
"ADDITIONAL_PYTHON_PATHS",
|
||||||
"INCLUDE_HEADERS",
|
"INCLUDE_HEADERS",
|
||||||
"WHITELIST_WORKSPACES",
|
"WHITELIST_WORKSPACES",
|
||||||
"BLACKLIST_WORKSPACES",
|
"BLACKLIST_WORKSPACES",
|
||||||
"NEW_USER_WEBHOOK",
|
"INSTANCE_EVENTS_WEBHOOK",
|
||||||
"CLOUD_HOSTED",
|
"CLOUD_HOSTED",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if server_mode || num_workers > 0 {
|
if server_mode || num_workers > 0 {
|
||||||
let addr = SocketAddr::from((server_bind_address, port));
|
let addr = SocketAddr::from((server_bind_address, port));
|
||||||
|
|
||||||
|
let rsmq2 = rsmq.clone();
|
||||||
let server_f = async {
|
let server_f = async {
|
||||||
if server_mode {
|
if server_mode {
|
||||||
windmill_api::run_server(db.clone(), addr, rx.resubscribe()).await?;
|
windmill_api::run_server(db.clone(), rsmq2, addr, rx.resubscribe()).await?;
|
||||||
}
|
}
|
||||||
Ok(()) as anyhow::Result<()>
|
Ok(()) as anyhow::Result<()>
|
||||||
};
|
};
|
||||||
@@ -138,29 +168,35 @@ Windmill Community Edition {GIT_VERSION}
|
|||||||
rx.resubscribe(),
|
rx.resubscribe(),
|
||||||
num_workers,
|
num_workers,
|
||||||
base_internal_url.clone(),
|
base_internal_url.clone(),
|
||||||
|
rsmq.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(()) as anyhow::Result<()>
|
Ok(()) as anyhow::Result<()>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rsmq2 = rsmq.clone();
|
||||||
let monitor_f = async {
|
let monitor_f = async {
|
||||||
if server_mode {
|
if server_mode {
|
||||||
monitor_db(&db, rx.resubscribe(), &base_internal_url);
|
monitor_db(&db, rx.resubscribe(), &base_internal_url, rsmq2);
|
||||||
}
|
}
|
||||||
Ok(()) as anyhow::Result<()>
|
Ok(()) as anyhow::Result<()>
|
||||||
};
|
};
|
||||||
|
|
||||||
let metrics_f = async {
|
let metrics_f = async {
|
||||||
match metrics_addr {
|
match metrics_addr {
|
||||||
Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
|
Some(addr) => {
|
||||||
.await
|
windmill_common::serve_metrics(addr, rx.resubscribe(), num_workers > 0)
|
||||||
.map_err(anyhow::Error::from),
|
.await
|
||||||
|
.map_err(anyhow::Error::from)
|
||||||
|
}
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
futures::try_join!(shutdown_signal, server_f, metrics_f, workers_f, monitor_f)?;
|
futures::try_join!(shutdown_signal, server_f, metrics_f, workers_f, monitor_f)?;
|
||||||
|
} else {
|
||||||
|
tracing::info!("Nothing to do, exiting.");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -182,10 +218,11 @@ fn display_config(envs: Vec<&str>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn monitor_db(
|
pub fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
rx: tokio::sync::broadcast::Receiver<()>,
|
rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
base_internal_url: &str,
|
base_internal_url: &str,
|
||||||
|
rsmq: Option<R>,
|
||||||
) {
|
) {
|
||||||
let db1 = db.clone();
|
let db1 = db.clone();
|
||||||
let db2 = db.clone();
|
let db2 = db.clone();
|
||||||
@@ -193,16 +230,17 @@ pub fn monitor_db(
|
|||||||
let rx2 = rx.resubscribe();
|
let rx2 = rx.resubscribe();
|
||||||
let base_internal_url = base_internal_url.to_string();
|
let base_internal_url = base_internal_url.to_string();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url).await
|
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq).await
|
||||||
});
|
});
|
||||||
tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await });
|
tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_workers(
|
pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
|
||||||
db: Pool<Postgres>,
|
db: Pool<Postgres>,
|
||||||
rx: tokio::sync::broadcast::Receiver<()>,
|
rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
num_workers: i32,
|
num_workers: i32,
|
||||||
base_internal_url: String,
|
base_internal_url: String,
|
||||||
|
rsmq: Option<R>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let license_key = std::env::var("LICENSE_KEY").ok();
|
let license_key = std::env::var("LICENSE_KEY").ok();
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
@@ -232,6 +270,7 @@ pub async fn run_workers(
|
|||||||
let ip = ip.clone();
|
let ip = ip.clone();
|
||||||
let rx = rx.resubscribe();
|
let rx = rx.resubscribe();
|
||||||
let base_internal_url = base_internal_url.clone();
|
let base_internal_url = base_internal_url.clone();
|
||||||
|
let rsmq2 = rsmq.clone();
|
||||||
handles.push(tokio::spawn(monitor.instrument(async move {
|
handles.push(tokio::spawn(monitor.instrument(async move {
|
||||||
tracing::info!(worker = %worker_name, "starting worker");
|
tracing::info!(worker = %worker_name, "starting worker");
|
||||||
windmill_worker::run_worker(
|
windmill_worker::run_worker(
|
||||||
@@ -242,6 +281,7 @@ pub async fn run_workers(
|
|||||||
&ip,
|
&ip,
|
||||||
rx,
|
rx,
|
||||||
&base_internal_url,
|
&base_internal_url,
|
||||||
|
rsmq2,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
})));
|
})));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use futures::{stream, Stream};
|
use futures::{stream, Stream};
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
|
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
|
||||||
use windmill_api::jobs::{CompletedJob, Job};
|
use windmill_api::jobs::{CompletedJob, Job};
|
||||||
@@ -87,7 +88,7 @@ impl ApiServer {
|
|||||||
let addr = sock.local_addr().unwrap();
|
let addr = sock.local_addr().unwrap();
|
||||||
drop(sock);
|
drop(sock);
|
||||||
|
|
||||||
let task = tokio::task::spawn(windmill_api::run_server(db.clone(), addr, rx));
|
let task = tokio::task::spawn(windmill_api::run_server(db.clone(), None, addr, rx));
|
||||||
|
|
||||||
return Self { addr, tx, task };
|
return Self { addr, tx, task };
|
||||||
}
|
}
|
||||||
@@ -154,12 +155,12 @@ mod suspend_resume {
|
|||||||
serde_json::from_value(serde_json::json!({
|
serde_json::from_value(serde_json::json!({
|
||||||
"modules": [{
|
"modules": [{
|
||||||
"id": "a",
|
"id": "a",
|
||||||
"input_transform": {
|
|
||||||
"n": { "type": "javascript", "expr": "flow_input.n", },
|
|
||||||
"port": { "type": "javascript", "expr": "flow_input.port", },
|
|
||||||
"op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", },
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": { "type": "javascript", "expr": "flow_input.n", },
|
||||||
|
"port": { "type": "javascript", "expr": "flow_input.port", },
|
||||||
|
"op": { "type": "javascript", "expr": "flow_input.op ?? 'resume'", },
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": "\
|
"content": "\
|
||||||
@@ -193,12 +194,12 @@ mod suspend_resume {
|
|||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
"id": "b",
|
"id": "b",
|
||||||
"input_transform": {
|
|
||||||
"n": { "type": "javascript", "expr": "results.a", },
|
|
||||||
"resume": { "type": "javascript", "expr": "resume", },
|
|
||||||
"resumes": { "type": "javascript", "expr": "resumes", },
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": { "type": "javascript", "expr": "results.a", },
|
||||||
|
"resume": { "type": "javascript", "expr": "resume", },
|
||||||
|
"resumes": { "type": "javascript", "expr": "resumes", },
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": "export function main(n, resume, resumes) { return { n: n + 1, resume, resumes } }"
|
"content": "export function main(n, resume, resumes) { return { n: n + 1, resume, resumes } }"
|
||||||
@@ -207,12 +208,12 @@ mod suspend_resume {
|
|||||||
"required_events": 1
|
"required_events": 1
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
"input_transform": {
|
|
||||||
"last": { "type": "javascript", "expr": "results.b", },
|
|
||||||
"resume": { "type": "javascript", "expr": "resume", },
|
|
||||||
"resumes": { "type": "javascript", "expr": "resumes", },
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"last": { "type": "javascript", "expr": "results.b", },
|
||||||
|
"resume": { "type": "javascript", "expr": "resume", },
|
||||||
|
"resumes": { "type": "javascript", "expr": "resumes", },
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": "export function main(last, resume, resumes) { return { last, resume, resumes } }"
|
"content": "export function main(last, resume, resumes) { return { last, resume, resumes } }"
|
||||||
@@ -252,9 +253,7 @@ mod suspend_resume {
|
|||||||
let second = completed.next().await.unwrap();
|
let second = completed.next().await.unwrap();
|
||||||
// print_job(second, &db).await;
|
// print_job(second, &db).await;
|
||||||
|
|
||||||
let tx = db.begin().await.unwrap();
|
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
|
||||||
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
|
|
||||||
tx.commit().await.unwrap();
|
|
||||||
let secret = reqwest::get(format!(
|
let secret = reqwest::get(format!(
|
||||||
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
|
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
|
||||||
))
|
))
|
||||||
@@ -355,9 +354,7 @@ mod suspend_resume {
|
|||||||
/* ... and send a request resume it. */
|
/* ... and send a request resume it. */
|
||||||
let second = completed.next().await.unwrap();
|
let second = completed.next().await.unwrap();
|
||||||
|
|
||||||
let tx = db.begin().await.unwrap();
|
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
|
||||||
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
|
|
||||||
tx.commit().await.unwrap();
|
|
||||||
let secret = reqwest::get(format!(
|
let secret = reqwest::get(format!(
|
||||||
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
|
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
|
||||||
))
|
))
|
||||||
@@ -477,11 +474,11 @@ def main(last, port):
|
|||||||
"iterator": { "type": "javascript", "expr": "flow_input.items" },
|
"iterator": { "type": "javascript", "expr": "flow_input.items" },
|
||||||
"skip_failures": false,
|
"skip_failures": false,
|
||||||
"modules": [{
|
"modules": [{
|
||||||
"input_transform": {
|
|
||||||
"index": { "type": "javascript", "expr": "flow_input.iter.index" },
|
|
||||||
"port": { "type": "javascript", "expr": "flow_input.port" },
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"index": { "type": "javascript", "expr": "flow_input.iter.index" },
|
||||||
|
"port": { "type": "javascript", "expr": "flow_input.port" },
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": inner_step(),
|
"content": inner_step(),
|
||||||
@@ -490,11 +487,11 @@ def main(last, port):
|
|||||||
},
|
},
|
||||||
"retry": { "constant": { "attempts": 2, "seconds": 0 } },
|
"retry": { "constant": { "attempts": 2, "seconds": 0 } },
|
||||||
}, {
|
}, {
|
||||||
"input_transform": {
|
|
||||||
"last": { "type": "javascript", "expr": "results.a" },
|
|
||||||
"port": { "type": "javascript", "expr": "flow_input.port" },
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"last": { "type": "javascript", "expr": "results.a" },
|
||||||
|
"port": { "type": "javascript", "expr": "flow_input.port" },
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": last_step(),
|
"content": last_step(),
|
||||||
@@ -632,7 +629,7 @@ def main(last, port):
|
|||||||
"modules": [{
|
"modules": [{
|
||||||
"id": "a",
|
"id": "a",
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": { "port": { "type": "javascript", "expr": "flow_input.port" } },
|
"input_transforms": { "port": { "type": "javascript", "expr": "flow_input.port" } },
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": r#"
|
"content": r#"
|
||||||
@@ -645,7 +642,7 @@ def main(port):
|
|||||||
}],
|
}],
|
||||||
"failure_module": {
|
"failure_module": {
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": { "error": { "type": "javascript", "expr": "previous_result", },
|
"input_transforms": { "error": { "type": "javascript", "expr": "previous_result", },
|
||||||
"port": { "type": "javascript", "expr": "flow_input.port" } },
|
"port": { "type": "javascript", "expr": "flow_input.port" } },
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
@@ -659,7 +656,7 @@ def main(error, port):
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (attempts, responses) = [
|
let (_attempts, responses) = [
|
||||||
/* fail the first step twice */
|
/* fail the first step twice */
|
||||||
(0x00, None),
|
(0x00, None),
|
||||||
(0x00, None),
|
(0x00, None),
|
||||||
@@ -681,14 +678,20 @@ def main(error, port):
|
|||||||
_ => panic!("expected failure module"),
|
_ => panic!("expected failure module"),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(server.close().await, attempts);
|
println!("result: {:#?}", result);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result
|
||||||
json!({
|
.get("from failure module")
|
||||||
"recv": 42,
|
.unwrap()
|
||||||
"from failure module": {"error": {"name": "IndexError", "stack": " File \"/tmp/inner.py\", line 5, in main\n return sock.recv(1)[0]\n", "message": "index out of range"}},
|
.get("error")
|
||||||
})
|
.unwrap()
|
||||||
|
.get("name")
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
json!("IndexError")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(result.get("recv").unwrap().clone(), json!(42));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,13 +708,13 @@ async fn test_iteration(db: Pool<Postgres>) {
|
|||||||
"iterator": { "type": "javascript", "expr": "result.items" },
|
"iterator": { "type": "javascript", "expr": "result.items" },
|
||||||
"skip_failures": false,
|
"skip_failures": false,
|
||||||
"modules": [{
|
"modules": [{
|
||||||
"input_transform": {
|
|
||||||
"n": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": "flow_input.iter.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": "flow_input.iter.value",
|
||||||
|
},
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
|
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
|
||||||
@@ -762,13 +765,13 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
|
|||||||
"skip_failures": false,
|
"skip_failures": false,
|
||||||
"parallel": true,
|
"parallel": true,
|
||||||
"modules": [{
|
"modules": [{
|
||||||
"input_transform": {
|
|
||||||
"n": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": "flow_input.iter.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": "flow_input.iter.value",
|
||||||
|
},
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
|
"content": "def main(n):\n if 1 < n:\n raise StopIteration(n)",
|
||||||
@@ -824,9 +827,8 @@ impl RunJob {
|
|||||||
|
|
||||||
async fn push(self, db: &Pool<Postgres>) -> Uuid {
|
async fn push(self, db: &Pool<Postgres>) -> Uuid {
|
||||||
let RunJob { payload, args } = self;
|
let RunJob { payload, args } = self;
|
||||||
let tx = db.begin().await.unwrap();
|
let (uuid, tx) = windmill_queue::push::<rsmq_async::MultiplexedRsmq>(
|
||||||
let (uuid, tx) = windmill_queue::push(
|
(None, db.begin().await.unwrap()).into(),
|
||||||
tx,
|
|
||||||
"test-workspace",
|
"test-workspace",
|
||||||
payload,
|
payload,
|
||||||
args,
|
args,
|
||||||
@@ -844,8 +846,7 @@ impl RunJob {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("push has to succeed");
|
.expect("push has to succeed");
|
||||||
|
tx.commit().await.unwrap();
|
||||||
tx.commit().await.expect("push has to commit");
|
|
||||||
|
|
||||||
uuid
|
uuid
|
||||||
}
|
}
|
||||||
@@ -915,7 +916,7 @@ fn spawn_test_worker(
|
|||||||
let ip: &str = Default::default();
|
let ip: &str = Default::default();
|
||||||
let future = async move {
|
let future = async move {
|
||||||
let base_internal_url = format!("http://localhost:{}", port);
|
let base_internal_url = format!("http://localhost:{}", port);
|
||||||
windmill_worker::run_worker(
|
windmill_worker::run_worker::<rsmq_async::MultiplexedRsmq>(
|
||||||
&db,
|
&db,
|
||||||
worker_instance,
|
worker_instance,
|
||||||
worker_name,
|
worker_name,
|
||||||
@@ -923,6 +924,7 @@ fn spawn_test_worker(
|
|||||||
ip,
|
ip,
|
||||||
rx,
|
rx,
|
||||||
&base_internal_url,
|
&base_internal_url,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
@@ -1001,7 +1003,6 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
|||||||
path: None,
|
path: None,
|
||||||
lock: None,
|
lock: None,
|
||||||
},
|
},
|
||||||
input_transforms: Default::default(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1029,7 +1030,6 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
|||||||
path: None,
|
path: None,
|
||||||
lock: None,
|
lock: None,
|
||||||
},
|
},
|
||||||
input_transforms: Default::default(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1037,7 +1037,6 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
|||||||
sleep: None,
|
sleep: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
input_transforms: Default::default(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1131,7 +1130,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
|||||||
path: None,
|
path: None,
|
||||||
lock: None,
|
lock: None,
|
||||||
},
|
},
|
||||||
input_transforms: Default::default(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1147,25 +1145,24 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
|||||||
modules: vec![
|
modules: vec![
|
||||||
FlowModule {
|
FlowModule {
|
||||||
id: "d".to_string(),
|
id: "d".to_string(),
|
||||||
input_transforms: [
|
|
||||||
(
|
|
||||||
"i".to_string(),
|
|
||||||
InputTransform::Javascript {
|
|
||||||
expr: "flow_input.iter.value".to_string(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"loop".to_string(),
|
|
||||||
InputTransform::Static { value: json!(true) },
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"path".to_string(),
|
|
||||||
InputTransform::Static { value: json!("inner.txt") },
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.into(),
|
|
||||||
value: FlowModuleValue::RawScript {
|
value: FlowModuleValue::RawScript {
|
||||||
input_transforms: [].into(),
|
input_transforms: [
|
||||||
|
(
|
||||||
|
"i".to_string(),
|
||||||
|
InputTransform::Javascript {
|
||||||
|
expr: "flow_input.iter.value".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"loop".to_string(),
|
||||||
|
InputTransform::Static { value: json!(true) },
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"path".to_string(),
|
||||||
|
InputTransform::Static { value: json!("inner.txt") },
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
language: ScriptLang::Deno,
|
language: ScriptLang::Deno,
|
||||||
content: write_file,
|
content: write_file,
|
||||||
path: None,
|
path: None,
|
||||||
@@ -1196,7 +1193,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
|||||||
path: None,
|
path: None,
|
||||||
lock: None,
|
lock: None,
|
||||||
},
|
},
|
||||||
input_transforms: [].into(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1205,7 +1201,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
input_transforms: Default::default(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1240,7 +1235,6 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
|||||||
path: None,
|
path: None,
|
||||||
lock: None,
|
lock: None,
|
||||||
},
|
},
|
||||||
input_transforms: [].into(),
|
|
||||||
stop_after_if: Default::default(),
|
stop_after_if: Default::default(),
|
||||||
summary: Default::default(),
|
summary: Default::default(),
|
||||||
suspend: Default::default(),
|
suspend: Default::default(),
|
||||||
@@ -1437,7 +1431,6 @@ async fn test_python_flow(db: Pool<Postgres>) {
|
|||||||
let doubles = "def main(n): return n * 2";
|
let doubles = "def main(n): return n * 2";
|
||||||
|
|
||||||
let flow: FlowValue = serde_json::from_value(serde_json::json!( {
|
let flow: FlowValue = serde_json::from_value(serde_json::json!( {
|
||||||
"input_transform": {},
|
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"value": {
|
"value": {
|
||||||
@@ -1453,16 +1446,16 @@ async fn test_python_flow(db: Pool<Postgres>) {
|
|||||||
"skip_failures": false,
|
"skip_failures": false,
|
||||||
"modules": [{
|
"modules": [{
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": "flow_input.iter.value",
|
||||||
|
},
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": doubles,
|
"content": doubles,
|
||||||
},
|
},
|
||||||
"input_transform": {
|
|
||||||
"n": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": "flow_input.iter.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1495,11 +1488,11 @@ async fn test_python_flow_2(db: Pool<Postgres>) {
|
|||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"content": "import wmill\ndef main(): return \"Hello\"",
|
"content": "import wmill\ndef main(): return \"Hello\"",
|
||||||
"language": "python3"
|
"language": "python3"
|
||||||
},
|
},
|
||||||
"input_transform": {}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
@@ -1527,6 +1520,8 @@ async fn test_go_job(db: Pool<Postgres>) {
|
|||||||
let port = server.addr.port();
|
let port = server.addr.port();
|
||||||
|
|
||||||
let content = r#"
|
let content = r#"
|
||||||
|
package inner
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
func main(derp string) (string, error) {
|
func main(derp string) (string, error) {
|
||||||
@@ -1678,7 +1673,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
|
|||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": {
|
"input_transforms": {
|
||||||
"n": {
|
"n": {
|
||||||
"type": "javascript",
|
"type": "javascript",
|
||||||
"expr": "flow_input.iter.value",
|
"expr": "flow_input.iter.value",
|
||||||
@@ -1694,7 +1689,7 @@ async fn test_empty_loop(db: Pool<Postgres>) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": {
|
"input_transforms": {
|
||||||
"items": {
|
"items": {
|
||||||
"type": "javascript",
|
"type": "javascript",
|
||||||
"expr": "results.a",
|
"expr": "results.a",
|
||||||
@@ -1771,13 +1766,13 @@ async fn test_empty_loop_2(db: Pool<Postgres>) {
|
|||||||
"iterator": { "type": "static", "value": [] },
|
"iterator": { "type": "static", "value": [] },
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"input_transform": {
|
|
||||||
"n": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": "flow_input.iter.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": "flow_input.iter.value",
|
||||||
|
},
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": "def main(n): return n",
|
"content": "def main(n): return n",
|
||||||
@@ -1813,13 +1808,13 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
|
|||||||
"iterator": { "type": "static", "value": [2,3,4] },
|
"iterator": { "type": "static", "value": [2,3,4] },
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"input_transform": {
|
|
||||||
"n": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": "flow_input.iter.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"n": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": "flow_input.iter.value",
|
||||||
|
},
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": "def main(n): return n",
|
"content": "def main(n): return n",
|
||||||
@@ -1829,13 +1824,13 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input_transform": {
|
|
||||||
"items": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": "results.a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"items": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": "results.a",
|
||||||
|
},
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "python3",
|
"language": "python3",
|
||||||
"content": "def main(items): return sum(items)",
|
"content": "def main(items): return sum(items)",
|
||||||
@@ -1857,17 +1852,17 @@ async fn test_step_after_loop(db: Pool<Postgres>) {
|
|||||||
fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
|
fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"id": format!("id_{}", i.to_string().replace("-", "_")),
|
"id": format!("id_{}", i.to_string().replace("-", "_")),
|
||||||
"input_transform": {
|
|
||||||
"array": {
|
|
||||||
"type": "javascript",
|
|
||||||
"expr": format!("results.{id}"),
|
|
||||||
},
|
|
||||||
"i": {
|
|
||||||
"type": "static",
|
|
||||||
"value": json!(i),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {
|
||||||
|
"array": {
|
||||||
|
"type": "javascript",
|
||||||
|
"expr": format!("results.{id}"),
|
||||||
|
},
|
||||||
|
"i": {
|
||||||
|
"type": "static",
|
||||||
|
"value": json!(i),
|
||||||
|
}
|
||||||
|
},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": "export function main(array, i){ array.push(i); return array }",
|
"content": "export function main(array, i){ array.push(i); return array }",
|
||||||
@@ -1877,8 +1872,8 @@ fn module_add_item_to_list(i: i32, id: &str) -> serde_json::Value {
|
|||||||
|
|
||||||
fn module_failure() -> serde_json::Value {
|
fn module_failure() -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"input_transform": {},
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": {},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": "export function main(){ throw Error('failure') }",
|
"content": "export function main(){ throw Error('failure') }",
|
||||||
@@ -2033,6 +2028,16 @@ async fn test_branchall_simple(db: Pool<Postgres>) {
|
|||||||
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
|
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorResult {
|
||||||
|
error: NamedError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct NamedError {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(fixtures("base"))]
|
#[sqlx::test(fixtures("base"))]
|
||||||
async fn test_branchall_skip_failure(db: Pool<Postgres>) {
|
async fn test_branchall_skip_failure(db: Pool<Postgres>) {
|
||||||
initialize_tracing().await;
|
initialize_tracing().await;
|
||||||
@@ -2068,8 +2073,11 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone())
|
||||||
serde_json::json!([{"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1,3]])
|
.unwrap()
|
||||||
|
.error
|
||||||
|
.name,
|
||||||
|
"Error"
|
||||||
);
|
);
|
||||||
|
|
||||||
let flow: FlowValue = serde_json::from_value(json!({
|
let flow: FlowValue = serde_json::from_value(json!({
|
||||||
@@ -2102,8 +2110,11 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
serde_json::from_value::<ErrorResult>(result.get(0).unwrap().clone())
|
||||||
serde_json::json!([ {"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1, 2]])
|
.unwrap()
|
||||||
|
.error
|
||||||
|
.name,
|
||||||
|
"Error"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2236,7 +2247,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
|
|||||||
"modules": [{
|
"modules": [{
|
||||||
"id": "a",
|
"id": "a",
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": {
|
"input_transforms": {
|
||||||
"l": { "type": "javascript", "expr": "[]", },
|
"l": { "type": "javascript", "expr": "[]", },
|
||||||
"n": { "type": "javascript", "expr": "flow_input.n", },
|
"n": { "type": "javascript", "expr": "flow_input.n", },
|
||||||
},
|
},
|
||||||
@@ -2247,7 +2258,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
|
|||||||
}, {
|
}, {
|
||||||
"id": "b",
|
"id": "b",
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": {
|
"input_transforms": {
|
||||||
"l": { "type": "javascript", "expr": "results.a.l", },
|
"l": { "type": "javascript", "expr": "results.a.l", },
|
||||||
"n": { "type": "javascript", "expr": "flow_input.n", },
|
"n": { "type": "javascript", "expr": "flow_input.n", },
|
||||||
},
|
},
|
||||||
@@ -2257,7 +2268,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
|
|||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
"value": {
|
"value": {
|
||||||
"input_transform": {
|
"input_transforms": {
|
||||||
"l": { "type": "javascript", "expr": "results.b.l", },
|
"l": { "type": "javascript", "expr": "results.b.l", },
|
||||||
"n": { "type": "javascript", "expr": "flow_input.n", },
|
"n": { "type": "javascript", "expr": "flow_input.n", },
|
||||||
},
|
},
|
||||||
@@ -2267,8 +2278,8 @@ async fn test_failure_module(db: Pool<Postgres>) {
|
|||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
"failure_module": {
|
"failure_module": {
|
||||||
"input_transform": { "error": { "type": "javascript", "expr": "previous_result", } },
|
|
||||||
"value": {
|
"value": {
|
||||||
|
"input_transforms": { "error": { "type": "javascript", "expr": "previous_result", } },
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
"language": "deno",
|
"language": "deno",
|
||||||
"content": "export function main(error) { return { 'from failure module': error } }",
|
"content": "export function main(error) { return { 'from failure module': error } }",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ tracing.workspace = true
|
|||||||
sql-builder.workspace = true
|
sql-builder.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
chrono-tz.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
serde_urlencoded.workspace = true
|
serde_urlencoded.workspace = true
|
||||||
@@ -69,3 +70,5 @@ async-stripe = { workspace = true, optional = true }
|
|||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
prometheus.workspace = true
|
prometheus.workspace = true
|
||||||
async_zip.workspace = true
|
async_zip.workspace = true
|
||||||
|
rsmq_async.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
openapi: "3.0.3"
|
openapi: "3.0.3"
|
||||||
|
|
||||||
info:
|
info:
|
||||||
version: 1.74.2
|
version: 1.87.0
|
||||||
title: Windmill API
|
title: Windmill API
|
||||||
|
|
||||||
contact:
|
contact:
|
||||||
@@ -117,9 +117,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: >
|
description: >
|
||||||
Successfully authenticated.
|
Successfully authenticated. The session ID is returned in a cookie
|
||||||
The session ID is returned in a cookie named `token` and as plaintext response.
|
named `token` and as plaintext response. Preferred method of
|
||||||
Preferred method of authorization is through the bearer token. The cookie is only for browser convenience.
|
authorization is through the bearer token. The cookie is only for
|
||||||
|
browser convenience.
|
||||||
|
|
||||||
headers:
|
headers:
|
||||||
Set-Cookie:
|
Set-Cookie:
|
||||||
@@ -204,7 +205,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
|
||||||
/w/{workspace}/users/is_owner/{path}:
|
/w/{workspace}/users/is_owner/{path}:
|
||||||
get:
|
get:
|
||||||
summary: is owner of path
|
summary: is owner of path
|
||||||
@@ -1048,6 +1048,27 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
/users/tokens/impersonate:
|
||||||
|
post:
|
||||||
|
summary: create token to impersonate a user (require superadmin)
|
||||||
|
operationId: createTokenImpersonate
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
requestBody:
|
||||||
|
description: new token
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/NewTokenImpersonate"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: token created
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
/users/tokens/delete/{token_prefix}:
|
/users/tokens/delete/{token_prefix}:
|
||||||
delete:
|
delete:
|
||||||
summary: delete token
|
summary: delete token
|
||||||
@@ -1258,9 +1279,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: >
|
description: >
|
||||||
Successfully authenticated.
|
Successfully authenticated. The session ID is returned in a cookie
|
||||||
The session ID is returned in a cookie named `token` and as plaintext response.
|
named `token` and as plaintext response. Preferred method of
|
||||||
Preferred method of authorization is through the bearer token. The cookie is only for browser convenience.
|
authorization is through the bearer token. The cookie is only for
|
||||||
|
browser convenience.
|
||||||
|
|
||||||
headers:
|
headers:
|
||||||
Set-Cookie:
|
Set-Cookie:
|
||||||
@@ -1549,7 +1571,7 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
value: {}
|
value: {}
|
||||||
@@ -2103,7 +2125,6 @@ paths:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
|
||||||
/w/{workspace}/scripts/create:
|
/w/{workspace}/scripts/create:
|
||||||
post:
|
post:
|
||||||
summary: create script
|
summary: create script
|
||||||
@@ -2344,6 +2365,24 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
/scripts_u/tokened_raw/{workspace}/{token}/{path}:
|
||||||
|
get:
|
||||||
|
summary: raw script by path with a token (mostly used by lsp to be used with import maps to resolve scripts)
|
||||||
|
operationId: rawScriptByPathTokened
|
||||||
|
tags:
|
||||||
|
- script
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/Token"
|
||||||
|
- $ref: "#/components/parameters/ScriptPath"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: script content
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
/w/{workspace}/scripts/exists/p/{path}:
|
/w/{workspace}/scripts/exists/p/{path}:
|
||||||
get:
|
get:
|
||||||
summary: exists script by path
|
summary: exists script by path
|
||||||
@@ -2469,17 +2508,6 @@ paths:
|
|||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/WorkspaceId"
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
- $ref: "#/components/parameters/ScriptPath"
|
- $ref: "#/components/parameters/ScriptPath"
|
||||||
- name: scheduled_for
|
|
||||||
description: when to schedule this job (leave empty for immediate run)
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
- name: scheduled_in_secs
|
|
||||||
description: schedule the script to execute in the number of seconds starting now
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
- $ref: "#/components/parameters/ParentJob"
|
- $ref: "#/components/parameters/ParentJob"
|
||||||
- $ref: "#/components/parameters/IncludeHeader"
|
- $ref: "#/components/parameters/IncludeHeader"
|
||||||
- $ref: "#/components/parameters/QueueLimit"
|
- $ref: "#/components/parameters/QueueLimit"
|
||||||
@@ -2499,6 +2527,26 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema: {}
|
schema: {}
|
||||||
|
|
||||||
|
get:
|
||||||
|
summary: run script by path with get
|
||||||
|
operationId: runWaitResultScriptByPathGet
|
||||||
|
tags:
|
||||||
|
- job
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/ScriptPath"
|
||||||
|
- $ref: "#/components/parameters/ParentJob"
|
||||||
|
- $ref: "#/components/parameters/IncludeHeader"
|
||||||
|
- $ref: "#/components/parameters/QueueLimit"
|
||||||
|
- $ref: "#/components/parameters/Payload"
|
||||||
|
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: job result
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
|
||||||
/w/{workspace}/jobs/run_wait_result/f/{path}:
|
/w/{workspace}/jobs/run_wait_result/f/{path}:
|
||||||
post:
|
post:
|
||||||
summary: run flow by path and wait until completion
|
summary: run flow by path and wait until completion
|
||||||
@@ -2508,17 +2556,6 @@ paths:
|
|||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/WorkspaceId"
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
- $ref: "#/components/parameters/ScriptPath"
|
- $ref: "#/components/parameters/ScriptPath"
|
||||||
- name: scheduled_for
|
|
||||||
description: when to schedule this job (leave empty for immediate run)
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
- name: scheduled_in_secs
|
|
||||||
description: schedule the script to execute in the number of seconds starting now
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
- $ref: "#/components/parameters/IncludeHeader"
|
- $ref: "#/components/parameters/IncludeHeader"
|
||||||
- $ref: "#/components/parameters/QueueLimit"
|
- $ref: "#/components/parameters/QueueLimit"
|
||||||
|
|
||||||
@@ -2745,6 +2782,27 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
/w/{workspace}/flows/input_history/p/{path}:
|
||||||
|
get:
|
||||||
|
summary: list inputs for previous completed flow jobs
|
||||||
|
operationId: getFlowInputHistoryByPath
|
||||||
|
tags:
|
||||||
|
- flow
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/ScriptPath"
|
||||||
|
- $ref: "#/components/parameters/Page"
|
||||||
|
- $ref: "#/components/parameters/PerPage"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: input history for completed jobs with this flow path
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Input"
|
||||||
|
|
||||||
/w/{workspace}/apps/list:
|
/w/{workspace}/apps/list:
|
||||||
get:
|
get:
|
||||||
summary: list all available apps
|
summary: list all available apps
|
||||||
@@ -2835,7 +2893,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/AppWithLastVersion"
|
$ref: "#/components/schemas/AppWithLastVersion"
|
||||||
|
|
||||||
|
|
||||||
/w/{workspace}/apps/secret_of/{path}:
|
/w/{workspace}/apps/secret_of/{path}:
|
||||||
get:
|
get:
|
||||||
summary: get public secret of app
|
summary: get public secret of app
|
||||||
@@ -3168,12 +3225,14 @@ paths:
|
|||||||
- $ref: "#/components/parameters/ScriptExactPath"
|
- $ref: "#/components/parameters/ScriptExactPath"
|
||||||
- $ref: "#/components/parameters/ScriptStartPath"
|
- $ref: "#/components/parameters/ScriptStartPath"
|
||||||
- $ref: "#/components/parameters/ScriptExactHash"
|
- $ref: "#/components/parameters/ScriptExactHash"
|
||||||
- $ref: "#/components/parameters/CreatedBefore"
|
- $ref: "#/components/parameters/StartedBefore"
|
||||||
- $ref: "#/components/parameters/CreatedAfter"
|
- $ref: "#/components/parameters/StartedAfter"
|
||||||
- $ref: "#/components/parameters/Success"
|
- $ref: "#/components/parameters/Success"
|
||||||
- $ref: "#/components/parameters/JobKinds"
|
- $ref: "#/components/parameters/JobKinds"
|
||||||
- $ref: "#/components/parameters/Suspended"
|
- $ref: "#/components/parameters/Suspended"
|
||||||
- $ref: "#/components/parameters/Running"
|
- $ref: "#/components/parameters/Running"
|
||||||
|
- $ref: "#/components/parameters/ArgsFilter"
|
||||||
|
- $ref: "#/components/parameters/ResultFilter"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: All available queued jobs
|
description: All available queued jobs
|
||||||
@@ -3198,10 +3257,12 @@ paths:
|
|||||||
- $ref: "#/components/parameters/ScriptExactPath"
|
- $ref: "#/components/parameters/ScriptExactPath"
|
||||||
- $ref: "#/components/parameters/ScriptStartPath"
|
- $ref: "#/components/parameters/ScriptStartPath"
|
||||||
- $ref: "#/components/parameters/ScriptExactHash"
|
- $ref: "#/components/parameters/ScriptExactHash"
|
||||||
- $ref: "#/components/parameters/CreatedBefore"
|
- $ref: "#/components/parameters/StartedBefore"
|
||||||
- $ref: "#/components/parameters/CreatedAfter"
|
- $ref: "#/components/parameters/StartedAfter"
|
||||||
- $ref: "#/components/parameters/Success"
|
- $ref: "#/components/parameters/Success"
|
||||||
- $ref: "#/components/parameters/JobKinds"
|
- $ref: "#/components/parameters/JobKinds"
|
||||||
|
- $ref: "#/components/parameters/ArgsFilter"
|
||||||
|
- $ref: "#/components/parameters/ResultFilter"
|
||||||
- name: is_skipped
|
- name: is_skipped
|
||||||
description: is the job skipped
|
description: is the job skipped
|
||||||
in: query
|
in: query
|
||||||
@@ -3235,9 +3296,11 @@ paths:
|
|||||||
- $ref: "#/components/parameters/ScriptExactPath"
|
- $ref: "#/components/parameters/ScriptExactPath"
|
||||||
- $ref: "#/components/parameters/ScriptStartPath"
|
- $ref: "#/components/parameters/ScriptStartPath"
|
||||||
- $ref: "#/components/parameters/ScriptExactHash"
|
- $ref: "#/components/parameters/ScriptExactHash"
|
||||||
- $ref: "#/components/parameters/CreatedBefore"
|
- $ref: "#/components/parameters/StartedBefore"
|
||||||
- $ref: "#/components/parameters/CreatedAfter"
|
- $ref: "#/components/parameters/StartedAfter"
|
||||||
- $ref: "#/components/parameters/JobKinds"
|
- $ref: "#/components/parameters/JobKinds"
|
||||||
|
- $ref: "#/components/parameters/ArgsFilter"
|
||||||
|
- $ref: "#/components/parameters/ResultFilter"
|
||||||
- name: is_skipped
|
- name: is_skipped
|
||||||
description: is the job skipped
|
description: is the job skipped
|
||||||
in: query
|
in: query
|
||||||
@@ -3280,23 +3343,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Job"
|
$ref: "#/components/schemas/Job"
|
||||||
|
|
||||||
# /w/{workspace}/jobs/flow/current_state/{id}:
|
|
||||||
# get:
|
|
||||||
# summary: get flow current step state
|
|
||||||
# operationId: getJob
|
|
||||||
# tags:
|
|
||||||
# - job
|
|
||||||
# parameters:
|
|
||||||
# - $ref: "#/components/parameters/WorkspaceId"
|
|
||||||
# - $ref: "#/components/parameters/JobId"
|
|
||||||
# responses:
|
|
||||||
# "200":
|
|
||||||
# description: state details
|
|
||||||
# content:
|
|
||||||
# application/json:
|
|
||||||
# schema:
|
|
||||||
# type: string
|
|
||||||
|
|
||||||
/w/{workspace}/jobs_u/getupdate/{id}:
|
/w/{workspace}/jobs_u/getupdate/{id}:
|
||||||
get:
|
get:
|
||||||
summary: get job updates
|
summary: get job updates
|
||||||
@@ -3382,7 +3428,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/CompletedJob"
|
$ref: "#/components/schemas/CompletedJob"
|
||||||
|
|
||||||
/w/{workspace}/jobs/queue/cancel/{id}:
|
/w/{workspace}/jobs_u/queue/cancel/{id}:
|
||||||
post:
|
post:
|
||||||
summary: cancel queued job
|
summary: cancel queued job
|
||||||
operationId: cancelQueuedJob
|
operationId: cancelQueuedJob
|
||||||
@@ -3410,6 +3456,34 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
/w/{workspace}/jobs_u/queue/force_cancel/{id}:
|
||||||
|
post:
|
||||||
|
summary: force cancel queued job
|
||||||
|
operationId: forceCancelQueuedJob
|
||||||
|
tags:
|
||||||
|
- job
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/JobId"
|
||||||
|
requestBody:
|
||||||
|
description: reason
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: job canceled
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
/w/{workspace}/jobs/job_signature/{id}/{resume_id}:
|
/w/{workspace}/jobs/job_signature/{id}/{resume_id}:
|
||||||
get:
|
get:
|
||||||
summary: create an HMac signature given a job id and a resume id
|
summary: create an HMac signature given a job id and a resume id
|
||||||
@@ -3438,7 +3512,7 @@ paths:
|
|||||||
|
|
||||||
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
|
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
|
||||||
get:
|
get:
|
||||||
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
|
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
|
||||||
operationId: getResumeUrls
|
operationId: getResumeUrls
|
||||||
tags:
|
tags:
|
||||||
- job
|
- job
|
||||||
@@ -3464,7 +3538,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
approvalPage:
|
approvalPage:
|
||||||
type: string
|
type: string
|
||||||
resume:
|
resume:
|
||||||
type: string
|
type: string
|
||||||
cancel:
|
cancel:
|
||||||
type: string
|
type: string
|
||||||
@@ -3482,6 +3556,7 @@ paths:
|
|||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/WorkspaceId"
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
- $ref: "#/components/parameters/JobId"
|
- $ref: "#/components/parameters/JobId"
|
||||||
|
- $ref: "#/components/parameters/Payload"
|
||||||
- name: resume_id
|
- name: resume_id
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
@@ -3695,13 +3770,14 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
schedule:
|
schedule:
|
||||||
type: string
|
type: string
|
||||||
offset:
|
timezone:
|
||||||
type: integer
|
type: string
|
||||||
required:
|
required:
|
||||||
- schedule
|
- schedule
|
||||||
|
- timezone
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: the preview of the next 10 time this schedule would apply to
|
description: List of 5 estimated upcoming execution events (in UTC)
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -4109,7 +4185,7 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
owners:
|
owners:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
extra_perms:
|
extra_perms:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@@ -4143,7 +4219,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
owners:
|
owners:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
extra_perms:
|
extra_perms:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@@ -4314,7 +4390,8 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [script, group_, resource, schedule, variable, flow, folder, app]
|
enum:
|
||||||
|
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: acls
|
description: acls
|
||||||
@@ -4339,7 +4416,8 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [script, group_, resource, schedule, variable, flow, folder, app]
|
enum:
|
||||||
|
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||||
requestBody:
|
requestBody:
|
||||||
description: acl to add
|
description: acl to add
|
||||||
required: true
|
required: true
|
||||||
@@ -4375,7 +4453,8 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [script, group_, resource, schedule, variable, flow, folder, app]
|
enum:
|
||||||
|
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||||
requestBody:
|
requestBody:
|
||||||
description: acl to add
|
description: acl to add
|
||||||
required: true
|
required: true
|
||||||
@@ -4395,19 +4474,18 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
|
||||||
/w/{workspace}/capture_u/{path}:
|
/w/{workspace}/capture_u/{path}:
|
||||||
post:
|
post:
|
||||||
summary: update flow preview capture
|
summary: update flow preview capture
|
||||||
operationId: updateCapture
|
operationId: updateCapture
|
||||||
tags:
|
tags:
|
||||||
- capture
|
- capture
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/WorkspaceId"
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
- $ref: "#/components/parameters/Path"
|
- $ref: "#/components/parameters/Path"
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: flow preview captured
|
description: flow preview captured
|
||||||
|
|
||||||
/w/{workspace}/capture/{path}:
|
/w/{workspace}/capture/{path}:
|
||||||
put:
|
put:
|
||||||
@@ -4484,6 +4562,118 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: unstar item
|
description: unstar item
|
||||||
|
|
||||||
|
/w/{workspace}/inputs/history:
|
||||||
|
get:
|
||||||
|
summary: List Inputs used in previously completed jobs
|
||||||
|
operationId: getInputHistory
|
||||||
|
tags:
|
||||||
|
- input
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/RunnableId"
|
||||||
|
- $ref: "#/components/parameters/RunnableType"
|
||||||
|
- $ref: "#/components/parameters/Page"
|
||||||
|
- $ref: "#/components/parameters/PerPage"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Input history for completed jobs
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Input"
|
||||||
|
|
||||||
|
/w/{workspace}/inputs/list:
|
||||||
|
get:
|
||||||
|
summary: List saved Inputs for a Runnable
|
||||||
|
operationId: listInputs
|
||||||
|
tags:
|
||||||
|
- input
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/RunnableId"
|
||||||
|
- $ref: "#/components/parameters/RunnableType"
|
||||||
|
- $ref: "#/components/parameters/Page"
|
||||||
|
- $ref: "#/components/parameters/PerPage"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Saved Inputs for a Runnable
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Input"
|
||||||
|
|
||||||
|
/w/{workspace}/inputs/create:
|
||||||
|
post:
|
||||||
|
summary: Create an Input for future use in a script or flow
|
||||||
|
operationId: createInput
|
||||||
|
tags:
|
||||||
|
- input
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/RunnableId"
|
||||||
|
- $ref: "#/components/parameters/RunnableType"
|
||||||
|
requestBody:
|
||||||
|
description: Input
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CreateInput"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Input created
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
|
||||||
|
/w/{workspace}/inputs/update:
|
||||||
|
post:
|
||||||
|
summary: Update an Input
|
||||||
|
operationId: updateInput
|
||||||
|
tags:
|
||||||
|
- input
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
requestBody:
|
||||||
|
description: UpdateInput
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UpdateInput"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Input updated
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
|
||||||
|
/w/{workspace}/inputs/delete/{input}:
|
||||||
|
post:
|
||||||
|
summary: Delete a Saved Input
|
||||||
|
operationId: deleteInput
|
||||||
|
tags:
|
||||||
|
- input
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/WorkspaceId"
|
||||||
|
- $ref: "#/components/parameters/InputId"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Input deleted
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
@@ -4493,6 +4683,7 @@ components:
|
|||||||
type: apiKey
|
type: apiKey
|
||||||
in: cookie
|
in: cookie
|
||||||
name: token
|
name: token
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
WorkspaceId:
|
WorkspaceId:
|
||||||
name: workspace
|
name: workspace
|
||||||
@@ -4500,6 +4691,12 @@ components:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
Token:
|
||||||
|
name: token
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
AccountId:
|
AccountId:
|
||||||
name: id
|
name: id
|
||||||
in: path
|
in: path
|
||||||
@@ -4575,7 +4772,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
ParentJob:
|
ParentJob:
|
||||||
name: parent_job
|
name: parent_job
|
||||||
description: The parent job that is at the origin and responsible for the execution of this script if any
|
description:
|
||||||
|
The parent job that is at the origin and responsible for the execution
|
||||||
|
of this script if any
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
@@ -4595,7 +4794,14 @@ components:
|
|||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
Payload:
|
||||||
|
name: payload
|
||||||
|
description: |
|
||||||
|
The base64 encoded payload that has been encoded as a JSON. e.g how to encode such payload encodeURIComponent
|
||||||
|
`encodeURIComponent(btoa(JSON.stringify({a: 2})))`
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
ScriptStartPath:
|
ScriptStartPath:
|
||||||
name: script_path_start
|
name: script_path_start
|
||||||
description: mask to filter matching starting path
|
description: mask to filter matching starting path
|
||||||
@@ -4614,15 +4820,15 @@ components:
|
|||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
CreatedBefore:
|
StartedBefore:
|
||||||
name: created_before
|
name: started_before
|
||||||
description: filter on created before (inclusive) timestamp
|
description: filter on created before (inclusive) timestamp
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
CreatedAfter:
|
StartedAfter:
|
||||||
name: created_after
|
name: started_after
|
||||||
description: filter on created after (exclusive) timestamp
|
description: filter on created after (exclusive) timestamp
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
@@ -4646,6 +4852,18 @@ components:
|
|||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
ArgsFilter:
|
||||||
|
name: args
|
||||||
|
description: filter on jobs containing those args as a json subset (@> in postgres)
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
ResultFilter:
|
||||||
|
name: result
|
||||||
|
description: filter on jobs containing those result as a json subset (@> in postgres)
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
After:
|
After:
|
||||||
name: after
|
name: after
|
||||||
description: filter on created after (exclusive) timestamp
|
description: filter on created after (exclusive) timestamp
|
||||||
@@ -4687,7 +4905,9 @@ components:
|
|||||||
enum: [Create, Update, Delete, Execute]
|
enum: [Create, Update, Delete, Execute]
|
||||||
JobKinds:
|
JobKinds:
|
||||||
name: job_kinds
|
name: job_kinds
|
||||||
description: filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by,
|
description:
|
||||||
|
filter on job kind (values 'preview', 'script', 'dependencies', 'flow')
|
||||||
|
separated by,
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
@@ -4697,9 +4917,26 @@ components:
|
|||||||
# type: string
|
# type: string
|
||||||
# enum: ["preview", "script", "dependencies"]
|
# enum: ["preview", "script", "dependencies"]
|
||||||
# explode: false
|
# explode: false
|
||||||
|
RunnableId:
|
||||||
|
name: runnable_id
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
RunnableType:
|
||||||
|
name: runnable_type
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RunnableType"
|
||||||
|
InputId:
|
||||||
|
name: input
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
$ref: "../../openflow.openapi.yaml#/components/schemas"
|
$ref: "../../openflow.openapi.yaml#/components/schemas"
|
||||||
|
|
||||||
Script:
|
Script:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -4770,6 +5007,60 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
|
|
||||||
|
Input:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
args:
|
||||||
|
type: object
|
||||||
|
created_by:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
is_public:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- args
|
||||||
|
- created_by
|
||||||
|
- created_at
|
||||||
|
- is_public
|
||||||
|
|
||||||
|
CreateInput:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
args:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- args
|
||||||
|
- created_by
|
||||||
|
|
||||||
|
UpdateInput:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
is_public:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- is_public
|
||||||
|
|
||||||
|
RunnableType:
|
||||||
|
type: string
|
||||||
|
enum: ["ScriptHash", "ScriptPath", "FlowPath"]
|
||||||
|
|
||||||
QueuedJob:
|
QueuedJob:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -5072,6 +5363,19 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
|
||||||
|
NewTokenImpersonate:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
expiration:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
impersonate_email:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- impersonate_email
|
||||||
|
|
||||||
ListableVariable:
|
ListableVariable:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -5197,6 +5501,11 @@ components:
|
|||||||
MainArgSignature:
|
MainArgSignature:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: ["Valid", "Invalid"]
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
star_args:
|
star_args:
|
||||||
type: boolean
|
type: boolean
|
||||||
star_kwargs:
|
star_kwargs:
|
||||||
@@ -5306,6 +5615,8 @@ components:
|
|||||||
- star_args
|
- star_args
|
||||||
- start_kwargs
|
- start_kwargs
|
||||||
- args
|
- args
|
||||||
|
- type
|
||||||
|
- error
|
||||||
|
|
||||||
Preview:
|
Preview:
|
||||||
type: object
|
type: object
|
||||||
@@ -5440,8 +5751,8 @@ components:
|
|||||||
format: date-time
|
format: date-time
|
||||||
schedule:
|
schedule:
|
||||||
type: string
|
type: string
|
||||||
offset_:
|
timezone:
|
||||||
type: integer
|
type: string
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
script_path:
|
script_path:
|
||||||
@@ -5464,7 +5775,7 @@ components:
|
|||||||
- edited_at
|
- edited_at
|
||||||
- schedule
|
- schedule
|
||||||
- script_path
|
- script_path
|
||||||
- offset_
|
- timezone
|
||||||
- extra_perms
|
- extra_perms
|
||||||
- is_flow
|
- is_flow
|
||||||
- enabled
|
- enabled
|
||||||
@@ -5477,8 +5788,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
schedule:
|
schedule:
|
||||||
type: string
|
type: string
|
||||||
offset:
|
timezone:
|
||||||
type: integer
|
type: string
|
||||||
script_path:
|
script_path:
|
||||||
type: string
|
type: string
|
||||||
is_flow:
|
is_flow:
|
||||||
@@ -5490,6 +5801,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- path
|
- path
|
||||||
- schedule
|
- schedule
|
||||||
|
- timezone
|
||||||
- script_path
|
- script_path
|
||||||
- is_flow
|
- is_flow
|
||||||
- args
|
- args
|
||||||
@@ -5499,10 +5811,13 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
schedule:
|
schedule:
|
||||||
type: string
|
type: string
|
||||||
|
timezone:
|
||||||
|
type: string
|
||||||
args:
|
args:
|
||||||
$ref: "#/components/schemas/ScriptArgs"
|
$ref: "#/components/schemas/ScriptArgs"
|
||||||
required:
|
required:
|
||||||
- schedule
|
- schedule
|
||||||
|
- timezone
|
||||||
- script_path
|
- script_path
|
||||||
- is_flow
|
- is_flow
|
||||||
- args
|
- args
|
||||||
@@ -5550,9 +5865,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
worker_instance:
|
worker_instance:
|
||||||
type: string
|
type: string
|
||||||
ping_at:
|
last_ping:
|
||||||
type: string
|
type: number
|
||||||
format: date-time
|
|
||||||
started_at:
|
started_at:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -5768,6 +6082,7 @@ components:
|
|||||||
- extra_perms
|
- extra_perms
|
||||||
- edited_at
|
- edited_at
|
||||||
- execution_mode
|
- execution_mode
|
||||||
|
|
||||||
AppWithLastVersion:
|
AppWithLastVersion:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ use windmill_common::{
|
|||||||
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
|
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use windmill_queue::{push, JobPayload, RawCode};
|
use windmill_queue::{push, JobPayload, QueueTransaction, RawCode};
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -320,6 +320,10 @@ async fn create_app(
|
|||||||
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
|
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
|
||||||
app.policy.on_behalf_of_email = Some(authed.email);
|
app.policy.on_behalf_of_email = Some(authed.email);
|
||||||
|
|
||||||
|
if &app.path == "" {
|
||||||
|
return Err(Error::BadRequest("App path cannot be empty".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
let id = sqlx::query_scalar!(
|
let id = sqlx::query_scalar!(
|
||||||
"INSERT INTO app
|
"INSERT INTO app
|
||||||
(workspace_id, path, summary, policy, versions)
|
(workspace_id, path, summary, policy, versions)
|
||||||
@@ -561,6 +565,7 @@ fn digest(code: &str) -> String {
|
|||||||
async fn execute_component(
|
async fn execute_component(
|
||||||
OptAuthed(opt_authed): OptAuthed,
|
OptAuthed(opt_authed): OptAuthed,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, path)): Path<(String, StripPath)>,
|
Path((w_id, path)): Path<(String, StripPath)>,
|
||||||
Json(payload): Json<ExecuteApp>,
|
Json(payload): Json<ExecuteApp>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
@@ -579,7 +584,7 @@ async fn execute_component(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let path = path.to_path();
|
let path = path.to_path();
|
||||||
let mut tx = db.begin().await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, db.begin().await?).into();
|
||||||
|
|
||||||
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
|
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
|
||||||
let mut hm = HashMap::new();
|
let mut hm = HashMap::new();
|
||||||
@@ -648,8 +653,12 @@ async fn execute_component(
|
|||||||
}
|
}
|
||||||
ExecuteApp { args, raw_code: None, path: Some(path), .. } => {
|
ExecuteApp { args, raw_code: None, path: Some(path), .. } => {
|
||||||
let payload = if path.starts_with("script/") {
|
let payload = if path.starts_with("script/") {
|
||||||
script_path_to_payload(path.strip_prefix("script/").unwrap(), &mut tx, &w_id)
|
script_path_to_payload(
|
||||||
.await?
|
path.strip_prefix("script/").unwrap(),
|
||||||
|
tx.transaction_mut(),
|
||||||
|
&w_id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
} else if path.starts_with("flow/") {
|
} else if path.starts_with("flow/") {
|
||||||
JobPayload::Flow(path.strip_prefix("flow/").unwrap().to_string())
|
JobPayload::Flow(path.strip_prefix("flow/").unwrap().to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -682,8 +691,8 @@ async fn execute_component(
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(uuid.to_string())
|
Ok(uuid.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,20 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use hyper::StatusCode;
|
use crate::{
|
||||||
use sql_builder::prelude::*;
|
db::{UserDB, DB},
|
||||||
|
schedule::clear_schedule,
|
||||||
|
users::{maybe_refresh_folders, require_owner_of_path, Authed},
|
||||||
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
|
HTTP_CLIENT,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, Path, Query},
|
extract::{Extension, Path, Query},
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use hyper::StatusCode;
|
||||||
|
use sql_builder::prelude::*;
|
||||||
use sql_builder::SqlBuilder;
|
use sql_builder::SqlBuilder;
|
||||||
use sqlx::{Postgres, Transaction};
|
use sqlx::{Postgres, Transaction};
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
@@ -25,15 +31,7 @@ use windmill_common::{
|
|||||||
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
|
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload};
|
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload, QueueTransaction};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db::{UserDB, DB},
|
|
||||||
schedule::clear_schedule,
|
|
||||||
users::{require_owner_of_path, Authed},
|
|
||||||
webhook_util::{WebhookMessage, WebhookShared},
|
|
||||||
HTTP_CLIENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -178,16 +176,20 @@ async fn check_path_conflict<'c>(
|
|||||||
|
|
||||||
async fn create_flow(
|
async fn create_flow(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(nf): Json<NewFlow>,
|
Json(nf): Json<NewFlow>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||||
let mut tx = user_db.clone().begin(&authed).await?;
|
let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await;
|
||||||
|
|
||||||
check_path_conflict(&mut tx, &w_id, &nf.path).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
|
||||||
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
|
|
||||||
|
check_path_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
|
||||||
|
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \
|
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \
|
||||||
@@ -219,13 +221,11 @@ async fn create_flow(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
|
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
|
||||||
);
|
);
|
||||||
|
|
||||||
let tx = user_db.begin(&authed).await?;
|
|
||||||
let (dependency_job_uuid, mut tx) = push(
|
let (dependency_job_uuid, mut tx) = push(
|
||||||
tx,
|
tx,
|
||||||
&w_id,
|
&w_id,
|
||||||
@@ -244,6 +244,7 @@ async fn create_flow(
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3",
|
"UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3",
|
||||||
dependency_job_uuid,
|
dependency_job_uuid,
|
||||||
@@ -283,15 +284,18 @@ async fn check_schedule_conflict<'c>(
|
|||||||
async fn update_flow(
|
async fn update_flow(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||||
Json(nf): Json<NewFlow>,
|
Json(nf): Json<NewFlow>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = user_db.clone().begin(&authed).await?;
|
|
||||||
|
|
||||||
let flow_path = flow_path.to_path();
|
let flow_path = flow_path.to_path();
|
||||||
check_schedule_conflict(&mut tx, &w_id, flow_path).await?;
|
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
|
||||||
|
|
||||||
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
|
||||||
|
|
||||||
|
check_schedule_conflict(tx.transaction_mut(), &w_id, flow_path).await?;
|
||||||
|
|
||||||
let schema = nf.schema.map(|x| x.0);
|
let schema = nf.schema.map(|x| x.0);
|
||||||
let old_dep_job = sqlx::query_scalar!(
|
let old_dep_job = sqlx::query_scalar!(
|
||||||
@@ -318,13 +322,13 @@ async fn update_flow(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if nf.path != flow_path {
|
if nf.path != flow_path {
|
||||||
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
|
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
|
||||||
|
|
||||||
if !authed.is_admin {
|
if !authed.is_admin {
|
||||||
require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?;
|
require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut schedulables = sqlx::query_as!(
|
let mut schedulables: Vec<Schedule> = sqlx::query_as!(
|
||||||
Schedule,
|
Schedule,
|
||||||
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
|
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
|
||||||
nf.path,
|
nf.path,
|
||||||
@@ -347,32 +351,32 @@ async fn update_flow(
|
|||||||
schedulables.push(schedule);
|
schedulables.push(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
for schedule in schedulables {
|
for schedule in schedulables.into_iter() {
|
||||||
clear_schedule(&mut tx, flow_path, true).await?;
|
// TODO: Why is this in the loop in the first place? Seems like it's just doing nothing after the first iteration? Should this use schedule.path?
|
||||||
|
clear_schedule(tx.transaction_mut(), flow_path, true).await?;
|
||||||
|
|
||||||
if schedule.enabled {
|
if schedule.enabled {
|
||||||
tx = push_scheduled_job(tx, schedule).await?;
|
tx = push_scheduled_job(tx, schedule).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audit_log(
|
||||||
|
&mut tx,
|
||||||
|
&authed.username,
|
||||||
|
"flows.update",
|
||||||
|
ActionKind::Create,
|
||||||
|
&w_id,
|
||||||
|
Some(&nf.path.to_string()),
|
||||||
|
Some(
|
||||||
|
[Some(("flow", nf.path.as_str()))]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
audit_log(
|
|
||||||
&mut tx,
|
|
||||||
&authed.username,
|
|
||||||
"flows.update",
|
|
||||||
ActionKind::Create,
|
|
||||||
&w_id,
|
|
||||||
Some(&nf.path.to_string()),
|
|
||||||
Some(
|
|
||||||
[Some(("flow", nf.path.as_str()))]
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::UpdateFlow {
|
WebhookMessage::UpdateFlow {
|
||||||
@@ -382,7 +386,6 @@ async fn update_flow(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let tx = user_db.begin(&authed).await?;
|
|
||||||
let (dependency_job_uuid, mut tx) = push(
|
let (dependency_job_uuid, mut tx) = push(
|
||||||
tx,
|
tx,
|
||||||
&w_id,
|
&w_id,
|
||||||
@@ -552,7 +555,6 @@ mod tests {
|
|||||||
modules: vec![
|
modules: vec![
|
||||||
FlowModule {
|
FlowModule {
|
||||||
id: "a".to_string(),
|
id: "a".to_string(),
|
||||||
input_transforms: [].into(),
|
|
||||||
value: FlowModuleValue::Script {
|
value: FlowModuleValue::Script {
|
||||||
path: "test".to_string(),
|
path: "test".to_string(),
|
||||||
input_transforms: [(
|
input_transforms: [(
|
||||||
@@ -570,7 +572,6 @@ mod tests {
|
|||||||
},
|
},
|
||||||
FlowModule {
|
FlowModule {
|
||||||
id: "b".to_string(),
|
id: "b".to_string(),
|
||||||
input_transforms: HashMap::new(),
|
|
||||||
value: FlowModuleValue::RawScript {
|
value: FlowModuleValue::RawScript {
|
||||||
input_transforms: HashMap::new(),
|
input_transforms: HashMap::new(),
|
||||||
content: "test".to_string(),
|
content: "test".to_string(),
|
||||||
@@ -589,7 +590,6 @@ mod tests {
|
|||||||
},
|
},
|
||||||
FlowModule {
|
FlowModule {
|
||||||
id: "c".to_string(),
|
id: "c".to_string(),
|
||||||
input_transforms: HashMap::new(),
|
|
||||||
value: FlowModuleValue::ForloopFlow {
|
value: FlowModuleValue::ForloopFlow {
|
||||||
iterator: InputTransform::Static { value: serde_json::json!([1, 2, 3]) },
|
iterator: InputTransform::Static { value: serde_json::json!([1, 2, 3]) },
|
||||||
modules: vec![],
|
modules: vec![],
|
||||||
@@ -608,7 +608,6 @@ mod tests {
|
|||||||
],
|
],
|
||||||
failure_module: Some(FlowModule {
|
failure_module: Some(FlowModule {
|
||||||
id: "d".to_string(),
|
id: "d".to_string(),
|
||||||
input_transforms: HashMap::new(),
|
|
||||||
value: FlowModuleValue::Script {
|
value: FlowModuleValue::Script {
|
||||||
path: "test".to_string(),
|
path: "test".to_string(),
|
||||||
input_transforms: HashMap::new(),
|
input_transforms: HashMap::new(),
|
||||||
@@ -629,7 +628,6 @@ mod tests {
|
|||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"id": "a",
|
"id": "a",
|
||||||
"input_transforms": {},
|
|
||||||
"value": {
|
"value": {
|
||||||
"input_transforms": {
|
"input_transforms": {
|
||||||
"test": {
|
"test": {
|
||||||
@@ -643,7 +641,6 @@ mod tests {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b",
|
"id": "b",
|
||||||
"input_transforms": {},
|
|
||||||
"value": {
|
"value": {
|
||||||
"input_transforms": {},
|
"input_transforms": {},
|
||||||
"type": "rawscript",
|
"type": "rawscript",
|
||||||
@@ -657,7 +654,6 @@ mod tests {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c",
|
"id": "c",
|
||||||
"input_transforms": {},
|
|
||||||
"value": {
|
"value": {
|
||||||
"type": "forloopflow",
|
"type": "forloopflow",
|
||||||
"iterator": {
|
"iterator": {
|
||||||
@@ -680,7 +676,6 @@ mod tests {
|
|||||||
],
|
],
|
||||||
"failure_module": {
|
"failure_module": {
|
||||||
"id": "d",
|
"id": "d",
|
||||||
"input_transforms": {},
|
|
||||||
"value": {
|
"value": {
|
||||||
"input_transforms": {},
|
"input_transforms": {},
|
||||||
"type": "script",
|
"type": "script",
|
||||||
@@ -695,31 +690,6 @@ mod tests {
|
|||||||
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
|
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_back_compat() {
|
|
||||||
/* renamed input_transform -> input_transforms but should deserialize old name */
|
|
||||||
let s = r#"
|
|
||||||
{
|
|
||||||
"value": {
|
|
||||||
"type": "rawscript",
|
|
||||||
"content": "def main(n): return",
|
|
||||||
"language": "python3"
|
|
||||||
},
|
|
||||||
"input_transform": {
|
|
||||||
"n": {
|
|
||||||
"expr": "flow_input.iter.value",
|
|
||||||
"type": "javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
let module: FlowModule = serde_json::from_str(s).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
module.input_transforms["n"],
|
|
||||||
InputTransform::Javascript { expr: "flow_input.iter.value".to_string() }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retry_serde() {
|
fn retry_serde() {
|
||||||
assert_eq!(Retry::default(), serde_json::from_str(r#"{}"#).unwrap());
|
assert_eq!(Retry::default(), serde_json::from_str(r#"{}"#).unwrap());
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
users::Authed,
|
users::{AuthCache, Authed, Tokened},
|
||||||
webhook_util::{WebhookMessage, WebhookShared},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -16,10 +18,11 @@ use axum::{
|
|||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{self, Error, JsonResult, Result},
|
error::{self, to_anyhow, Error, JsonResult, Result},
|
||||||
users::username_to_permissioned_as,
|
users::username_to_permissioned_as,
|
||||||
utils::{not_found_if_none, paginate, Pagination},
|
utils::{not_found_if_none, paginate, Pagination},
|
||||||
};
|
};
|
||||||
@@ -137,16 +140,28 @@ async fn check_name_conflict<'c>(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref VALID_FOLDER_NAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_folder(
|
async fn create_folder(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
|
Tokened { token }: Tokened,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
|
Extension(cache): Extension<Arc<AuthCache>>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(ng): Json<NewFolder>,
|
Json(ng): Json<NewFolder>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
if !VALID_FOLDER_NAME.is_match(&ng.name) {
|
||||||
|
return Err(windmill_common::error::Error::BadRequest(format!(
|
||||||
|
"Folder name can only contain alphanumeric characters, underscores"
|
||||||
|
)));
|
||||||
|
}
|
||||||
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
|
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
|
||||||
|
cache.invalidate(&w_id, token).await;
|
||||||
let owner = username_to_permissioned_as(&authed.username);
|
let owner = username_to_permissioned_as(&authed.username);
|
||||||
let owners = &ng.owners.unwrap_or(vec![owner.clone()]);
|
let owners = &ng.owners.unwrap_or(vec![owner.clone()]);
|
||||||
|
|
||||||
@@ -262,13 +277,26 @@ async fn update_folder(
|
|||||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||||
|
|
||||||
if let Some(display_name) = ng.display_name {
|
if let Some(display_name) = ng.display_name {
|
||||||
sqlb.set("display_name", display_name);
|
sqlb.set("display_name", "?".bind(&display_name));
|
||||||
}
|
}
|
||||||
if let Some(owners) = ng.owners {
|
if let Some(owners) = ng.owners {
|
||||||
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
|
sqlb.set(
|
||||||
|
"owners",
|
||||||
|
"?".bind(&format!(
|
||||||
|
"{{{}}}",
|
||||||
|
owners
|
||||||
|
.iter()
|
||||||
|
.map(|x| format!("\"{x}\""))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(","),
|
||||||
|
)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(extra_perms) = ng.extra_perms {
|
if let Some(extra_perms) = ng.extra_perms {
|
||||||
sqlb.set_str("extra_perms", extra_perms.to_string());
|
sqlb.set(
|
||||||
|
"extra_perms",
|
||||||
|
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlb.returning("*");
|
sqlb.returning("*");
|
||||||
|
|||||||
282
backend/windmill-api/src/inputs.rs
Normal file
282
backend/windmill-api/src/inputs.rs
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/*
|
||||||
|
* Author: Ruben Fiszel
|
||||||
|
* Copyright: Windmill Labs, Inc 2022
|
||||||
|
* This file and its contents are licensed under the AGPLv3 License.
|
||||||
|
* Please see the included NOTICE for copyright information and
|
||||||
|
* LICENSE-AGPL for a copy of the license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{db::UserDB, jobs::CompletedJob, users::Authed};
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query},
|
||||||
|
routing::{get, post},
|
||||||
|
Extension, Json, Router,
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::types::Uuid;
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
vec,
|
||||||
|
};
|
||||||
|
use windmill_common::{
|
||||||
|
error::JsonResult,
|
||||||
|
scripts::to_i64,
|
||||||
|
utils::{paginate, Pagination},
|
||||||
|
};
|
||||||
|
use windmill_queue::JobKind;
|
||||||
|
|
||||||
|
pub fn workspaced_service() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/history", get(get_input_history))
|
||||||
|
.route("/list", get(list_saved_inputs))
|
||||||
|
.route("/create", post(create_input))
|
||||||
|
.route("/update", post(update_input))
|
||||||
|
.route("/delete/:id", post(delete_input))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)]
|
||||||
|
pub struct InputRow {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub runnable_id: String,
|
||||||
|
pub runnable_type: RunnableType,
|
||||||
|
pub name: String,
|
||||||
|
pub args: Value,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub created_by: String,
|
||||||
|
pub is_public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
|
||||||
|
#[sqlx(type_name = "runnable_type")]
|
||||||
|
pub enum RunnableType {
|
||||||
|
ScriptHash,
|
||||||
|
ScriptPath,
|
||||||
|
FlowPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RunnableType {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
RunnableType::ScriptHash => write!(f, "ScriptHash"),
|
||||||
|
RunnableType::ScriptPath => write!(f, "ScriptPath"),
|
||||||
|
RunnableType::FlowPath => write!(f, "FlowPath"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunnableType {
|
||||||
|
fn job_kind(&self) -> JobKind {
|
||||||
|
match self {
|
||||||
|
RunnableType::ScriptHash => JobKind::Script,
|
||||||
|
RunnableType::ScriptPath => JobKind::Script,
|
||||||
|
RunnableType::FlowPath => JobKind::Flow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
RunnableType::ScriptHash => "script_hash",
|
||||||
|
RunnableType::ScriptPath => "script_path",
|
||||||
|
RunnableType::FlowPath => "script_path",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RunnableParams {
|
||||||
|
pub runnable_id: String,
|
||||||
|
pub runnable_type: RunnableType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Input {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
args: serde_json::Value,
|
||||||
|
created_by: String,
|
||||||
|
is_public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_input_history(
|
||||||
|
authed: Authed,
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Path(w_id): Path<String>,
|
||||||
|
Query(pagination): Query<Pagination>,
|
||||||
|
Query(r): Query<RunnableParams>,
|
||||||
|
) -> JsonResult<Vec<Input>> {
|
||||||
|
let (per_page, offset) = paginate(pagination);
|
||||||
|
|
||||||
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
let sql = &format!(
|
||||||
|
"select distinct on (args) * from completed_job \
|
||||||
|
where {} = $1 and job_kind = $2 and workspace_id = $3 \
|
||||||
|
order by args, started_at desc limit $4 offset $5",
|
||||||
|
r.runnable_type.column_name()
|
||||||
|
);
|
||||||
|
|
||||||
|
let query = sqlx::query_as::<_, CompletedJob>(sql);
|
||||||
|
|
||||||
|
let query = match r.runnable_type {
|
||||||
|
RunnableType::ScriptHash => query.bind(to_i64(&r.runnable_id)?),
|
||||||
|
_ => query.bind(&r.runnable_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rows = query
|
||||||
|
.bind(r.runnable_type.job_kind())
|
||||||
|
.bind(&w_id)
|
||||||
|
.bind(per_page as i32)
|
||||||
|
.bind(offset as i32)
|
||||||
|
.fetch_all(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
let mut inputs = vec![];
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
inputs.push(Input {
|
||||||
|
id: row.id,
|
||||||
|
name: format!(
|
||||||
|
"{} {}",
|
||||||
|
row.created_at.format("%H:%M %-d/%-m"),
|
||||||
|
row.created_by
|
||||||
|
),
|
||||||
|
created_at: row.created_at,
|
||||||
|
args: row.args.unwrap_or(serde_json::json!({})),
|
||||||
|
created_by: row.created_by,
|
||||||
|
is_public: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_saved_inputs(
|
||||||
|
authed: Authed,
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Path(w_id): Path<String>,
|
||||||
|
Query(pagination): Query<Pagination>,
|
||||||
|
Query(r): Query<RunnableParams>,
|
||||||
|
) -> JsonResult<Vec<Input>> {
|
||||||
|
let (per_page, offset) = paginate(pagination);
|
||||||
|
|
||||||
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as::<_, InputRow>(
|
||||||
|
"select * from input \
|
||||||
|
where runnable_id = $1 and runnable_type = $2 and workspace_id = $3 \
|
||||||
|
and is_public IS true OR created_by = $4 \
|
||||||
|
order by created_at desc limit $5 offset $6",
|
||||||
|
)
|
||||||
|
.bind(&r.runnable_id)
|
||||||
|
.bind(&r.runnable_type)
|
||||||
|
.bind(&w_id)
|
||||||
|
.bind(&authed.username)
|
||||||
|
.bind(per_page as i32)
|
||||||
|
.bind(offset as i32)
|
||||||
|
.fetch_all(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
let mut inputs: Vec<Input> = Vec::new();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
inputs.push(Input {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
args: row.args,
|
||||||
|
created_by: row.created_by,
|
||||||
|
created_at: row.created_at,
|
||||||
|
is_public: row.is_public,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CreateInput {
|
||||||
|
name: String,
|
||||||
|
args: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_input(
|
||||||
|
authed: Authed,
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Path(w_id): Path<String>,
|
||||||
|
Query(r): Query<RunnableParams>,
|
||||||
|
Json(input): Json<CreateInput>,
|
||||||
|
) -> JsonResult<String> {
|
||||||
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO input (id, workspace_id, runnable_id, runnable_type, name, args, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
|
)
|
||||||
|
.bind(&id)
|
||||||
|
.bind(&w_id)
|
||||||
|
.bind(&r.runnable_id)
|
||||||
|
.bind(&r.runnable_type)
|
||||||
|
.bind(&input.name)
|
||||||
|
.bind(&input.args)
|
||||||
|
.bind(&authed.username)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct UpdateInput {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
is_public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_input(
|
||||||
|
authed: Authed,
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Path(w_id): Path<String>,
|
||||||
|
Json(input): Json<UpdateInput>,
|
||||||
|
) -> JsonResult<String> {
|
||||||
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4")
|
||||||
|
.bind(&input.name)
|
||||||
|
.bind(&input.is_public)
|
||||||
|
.bind(&input.id)
|
||||||
|
.bind(&w_id)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(input.id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_input(
|
||||||
|
authed: Authed,
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Path((w_id, i_id)): Path<(String, Uuid)>,
|
||||||
|
) -> JsonResult<String> {
|
||||||
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2")
|
||||||
|
.bind(&i_id)
|
||||||
|
.bind(&w_id)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(i_id.to_string()))
|
||||||
|
}
|
||||||
@@ -6,6 +6,12 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{UserDB, DB},
|
||||||
|
users::{require_owner_of_path, Authed, OptAuthed},
|
||||||
|
variables::get_workspace_key,
|
||||||
|
BASE_URL,
|
||||||
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{FromRequest, Json, Path, Query},
|
extract::{FromRequest, Json, Path, Query},
|
||||||
@@ -30,13 +36,8 @@ use windmill_common::{
|
|||||||
users::username_to_permissioned_as,
|
users::username_to_permissioned_as,
|
||||||
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
|
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
|
||||||
};
|
};
|
||||||
use windmill_queue::{get_queued_job, push, JobKind, JobPayload, QueuedJob, RawCode};
|
use windmill_queue::{
|
||||||
|
get_queued_job, push, JobKind, JobPayload, QueueTransaction, QueuedJob, RawCode,
|
||||||
use crate::{
|
|
||||||
db::{UserDB, DB},
|
|
||||||
users::{require_owner_of_path, Authed},
|
|
||||||
variables::get_workspace_key,
|
|
||||||
BASE_URL,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
@@ -47,6 +48,10 @@ pub fn workspaced_service() -> Router {
|
|||||||
"/run_wait_result/p/*script_path",
|
"/run_wait_result/p/*script_path",
|
||||||
post(run_wait_result_job_by_path),
|
post(run_wait_result_job_by_path),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/run_wait_result/p/*script_path",
|
||||||
|
get(run_wait_result_job_by_path_get),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/run_wait_result/h/:hash",
|
"/run_wait_result/h/:hash",
|
||||||
post(run_wait_result_job_by_hash),
|
post(run_wait_result_job_by_hash),
|
||||||
@@ -60,7 +65,7 @@ pub fn workspaced_service() -> Router {
|
|||||||
.route("/run/preview_flow", post(run_preview_flow_job))
|
.route("/run/preview_flow", post(run_preview_flow_job))
|
||||||
.route("/list", get(list_jobs))
|
.route("/list", get(list_jobs))
|
||||||
.route("/queue/list", get(list_queue_jobs))
|
.route("/queue/list", get(list_queue_jobs))
|
||||||
.route("/queue/cancel/:id", post(cancel_job_api))
|
.route("/queue/count", get(count_queue_jobs))
|
||||||
.route("/completed/list", get(list_completed_jobs))
|
.route("/completed/list", get(list_completed_jobs))
|
||||||
.route("/completed/get/:id", get(get_completed_job))
|
.route("/completed/get/:id", get(get_completed_job))
|
||||||
.route("/completed/get_result/:id", get(get_completed_job_result))
|
.route("/completed/get_result/:id", get(get_completed_job_result))
|
||||||
@@ -98,6 +103,8 @@ pub fn global_service() -> Router {
|
|||||||
)
|
)
|
||||||
.route("/get/:id", get(get_job))
|
.route("/get/:id", get(get_job))
|
||||||
.route("/getupdate/:id", get(get_job_update))
|
.route("/getupdate/:id", get(get_job_update))
|
||||||
|
.route("/queue/cancel/:id", post(cancel_job_api))
|
||||||
|
.route("/queue/force_cancel/:id", post(force_cancel))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_result_by_id(
|
async fn get_result_by_id(
|
||||||
@@ -109,20 +116,26 @@ async fn get_result_by_id(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn cancel_job_api(
|
async fn cancel_job_api(
|
||||||
authed: Authed,
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
OptAuthed(opt_authed): OptAuthed,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
Path((w_id, id)): Path<(String, Uuid)>,
|
Path((w_id, id)): Path<(String, Uuid)>,
|
||||||
Json(CancelJob { reason }): Json<CancelJob>,
|
Json(CancelJob { reason }): Json<CancelJob>,
|
||||||
) -> error::Result<String> {
|
) -> error::Result<String> {
|
||||||
let tx = user_db.begin(&authed).await?;
|
let tx = db.begin().await?;
|
||||||
|
|
||||||
|
let username = match opt_authed {
|
||||||
|
Some(authed) => authed.username,
|
||||||
|
None => "anonymous".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let (mut tx, job_option) =
|
let (mut tx, job_option) =
|
||||||
windmill_queue::cancel_job(&authed.username, reason, id, &w_id, tx).await?;
|
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, false).await?;
|
||||||
|
|
||||||
if let Some(id) = job_option {
|
if let Some(id) = job_option {
|
||||||
audit_log(
|
audit_log(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&authed.username,
|
&username,
|
||||||
"jobs.cancel",
|
"jobs.cancel",
|
||||||
ActionKind::Delete,
|
ActionKind::Delete,
|
||||||
&w_id,
|
&w_id,
|
||||||
@@ -145,6 +158,49 @@ async fn cancel_job_api(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn force_cancel(
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
|
OptAuthed(opt_authed): OptAuthed,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
|
Path((w_id, id)): Path<(String, Uuid)>,
|
||||||
|
Json(CancelJob { reason }): Json<CancelJob>,
|
||||||
|
) -> error::Result<String> {
|
||||||
|
let tx = db.begin().await?;
|
||||||
|
|
||||||
|
let username = match opt_authed {
|
||||||
|
Some(authed) => authed.username,
|
||||||
|
None => "anonymous".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut tx, job_option) =
|
||||||
|
windmill_queue::cancel_job(&username, reason, id, &w_id, tx, rsmq, true).await?;
|
||||||
|
|
||||||
|
if let Some(id) = job_option {
|
||||||
|
audit_log(
|
||||||
|
&mut tx,
|
||||||
|
&username,
|
||||||
|
"jobs.force_cancel",
|
||||||
|
ActionKind::Delete,
|
||||||
|
&w_id,
|
||||||
|
Some(&id.to_string()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(id.to_string())
|
||||||
|
} else {
|
||||||
|
let (job_o, tx) = get_job_by_id(tx, &w_id, id).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
let err = match job_o {
|
||||||
|
Some(Job::CompletedJob(_)) => {
|
||||||
|
return Ok(format!("queued job id {} is already completed", id))
|
||||||
|
}
|
||||||
|
_ => error::Error::NotFound(format!("queued job id {} does not exist", id)),
|
||||||
|
};
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_path_for_hash<'c>(
|
pub async fn get_path_for_hash<'c>(
|
||||||
db: &mut Transaction<'c, Postgres>,
|
db: &mut Transaction<'c, Postgres>,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
@@ -261,6 +317,7 @@ pub struct RunJobQuery {
|
|||||||
include_header: Option<String>,
|
include_header: Option<String>,
|
||||||
invisible_to_owner: Option<bool>,
|
invisible_to_owner: Option<bool>,
|
||||||
queue_limit: Option<i64>,
|
queue_limit: Option<i64>,
|
||||||
|
payload: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -300,9 +357,6 @@ pub fn add_include_headers(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
mut args: serde_json::Map<String, serde_json::Value>,
|
mut args: serde_json::Map<String, serde_json::Value>,
|
||||||
) -> serde_json::Map<String, serde_json::Value> {
|
) -> serde_json::Map<String, serde_json::Value> {
|
||||||
if include_header.is_none() {
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
let whitelist = include_header
|
let whitelist = include_header
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
|
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
|
||||||
@@ -321,19 +375,22 @@ pub fn add_include_headers(
|
|||||||
});
|
});
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ListQueueQuery {
|
pub struct ListQueueQuery {
|
||||||
pub script_path_start: Option<String>,
|
pub script_path_start: Option<String>,
|
||||||
pub script_path_exact: Option<String>,
|
pub script_path_exact: Option<String>,
|
||||||
pub script_hash: Option<String>,
|
pub script_hash: Option<String>,
|
||||||
pub created_by: Option<String>,
|
pub created_by: Option<String>,
|
||||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
pub running: Option<bool>,
|
pub running: Option<bool>,
|
||||||
pub parent_job: Option<String>,
|
pub parent_job: Option<String>,
|
||||||
pub order_desc: Option<bool>,
|
pub order_desc: Option<bool>,
|
||||||
pub job_kinds: Option<String>,
|
pub job_kinds: Option<String>,
|
||||||
pub suspended: Option<bool>,
|
pub suspended: Option<bool>,
|
||||||
|
// filter by matching a subset of the args using base64 encoded json subset
|
||||||
|
pub args: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
|
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
|
||||||
@@ -362,11 +419,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
|||||||
if let Some(pj) = &lq.parent_job {
|
if let Some(pj) = &lq.parent_job {
|
||||||
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
||||||
}
|
}
|
||||||
if let Some(dt) = &lq.created_before {
|
if let Some(dt) = &lq.started_before {
|
||||||
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||||
}
|
}
|
||||||
if let Some(dt) = &lq.created_after {
|
if let Some(dt) = &lq.started_after {
|
||||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(s) = &lq.suspended {
|
if let Some(s) = &lq.suspended {
|
||||||
@@ -376,6 +433,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
|||||||
sqlb.and_where_eq("suspend", 0);
|
sqlb.and_where_eq("suspend", 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(jk) = &lq.job_kinds {
|
if let Some(jk) = &lq.job_kinds {
|
||||||
sqlb.and_where_in(
|
sqlb.and_where_in(
|
||||||
"job_kind",
|
"job_kind",
|
||||||
@@ -383,6 +441,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(args) = &lq.args {
|
||||||
|
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||||
|
}
|
||||||
|
|
||||||
sqlb
|
sqlb
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +500,26 @@ async fn list_queue_jobs(
|
|||||||
Ok(Json(jobs))
|
Ok(Json(jobs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, FromRow)]
|
||||||
|
struct QueueStats {
|
||||||
|
database_length: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count_queue_jobs(
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
|
Path(w_id): Path<String>,
|
||||||
|
) -> error::JsonResult<QueueStats> {
|
||||||
|
Ok(Json(
|
||||||
|
sqlx::query_as!(
|
||||||
|
QueueStats,
|
||||||
|
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1",
|
||||||
|
w_id
|
||||||
|
)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_jobs(
|
async fn list_jobs(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
@@ -455,13 +537,14 @@ async fn list_jobs(
|
|||||||
script_path_exact: lq.script_path_exact,
|
script_path_exact: lq.script_path_exact,
|
||||||
script_hash: lq.script_hash,
|
script_hash: lq.script_hash,
|
||||||
created_by: lq.created_by,
|
created_by: lq.created_by,
|
||||||
created_before: lq.created_before,
|
started_before: lq.started_before,
|
||||||
created_after: lq.created_after,
|
started_after: lq.started_after,
|
||||||
running: None,
|
running: None,
|
||||||
parent_job: lq.parent_job,
|
parent_job: lq.parent_job,
|
||||||
order_desc: Some(true),
|
order_desc: Some(true),
|
||||||
job_kinds: lq.job_kinds,
|
job_kinds: lq.job_kinds,
|
||||||
suspended: lq.suspended,
|
suspended: lq.suspended,
|
||||||
|
args: lq.args,
|
||||||
},
|
},
|
||||||
&[
|
&[
|
||||||
"'QueuedJob' as typ",
|
"'QueuedJob' as typ",
|
||||||
@@ -734,6 +817,7 @@ async fn get_suspended_flow_info<'c>(
|
|||||||
pub async fn cancel_suspended_job(
|
pub async fn cancel_suspended_job(
|
||||||
/* unauthed */
|
/* unauthed */
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
|
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
|
||||||
Query(approver): Query<QueryApprover>,
|
Query(approver): Query<QueryApprover>,
|
||||||
) -> error::Result<String> {
|
) -> error::Result<String> {
|
||||||
@@ -756,6 +840,8 @@ pub async fn cancel_suspended_job(
|
|||||||
parent_flow,
|
parent_flow,
|
||||||
&w_id,
|
&w_id,
|
||||||
tx,
|
tx,
|
||||||
|
rsmq,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if job.is_some() {
|
if job.is_some() {
|
||||||
@@ -1136,26 +1222,27 @@ where
|
|||||||
struct InPayload {
|
struct InPayload {
|
||||||
payload: Option<String>,
|
payload: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
|
|
||||||
let vec = base64::engine::general_purpose::URL_SAFE
|
|
||||||
.decode(t)
|
|
||||||
.context("invalid base64")?;
|
|
||||||
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> {
|
||||||
|
let vec = base64::engine::general_purpose::URL_SAFE
|
||||||
|
.decode(t)
|
||||||
|
.context("invalid base64")?;
|
||||||
|
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
||||||
|
}
|
||||||
pub async fn run_flow_by_path(
|
pub async fn run_flow_by_path(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
||||||
) -> error::Result<(StatusCode, String)> {
|
) -> error::Result<(StatusCode, String)> {
|
||||||
let flow_path = flow_path.to_path();
|
let flow_path = flow_path.to_path();
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1183,15 +1270,16 @@ pub async fn run_flow_by_path(
|
|||||||
pub async fn run_job_by_path(
|
pub async fn run_job_by_path(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, script_path)): Path<(String, StripPath)>,
|
Path((w_id, script_path)): Path<(String, StripPath)>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
||||||
) -> error::Result<(StatusCode, String)> {
|
) -> error::Result<(StatusCode, String)> {
|
||||||
let script_path = script_path.to_path();
|
let script_path = script_path.to_path();
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
|
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1330,9 +1418,65 @@ lazy_static::lazy_static! {
|
|||||||
.and_then(|x| x.parse().ok())
|
.and_then(|x| x.parse().ok())
|
||||||
.unwrap_or(20);
|
.unwrap_or(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run_wait_result_job_by_path_get(
|
||||||
|
authed: Authed,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
|
Path((w_id, script_path)): Path<(String, StripPath)>,
|
||||||
|
Query(run_query): Query<RunJobQuery>,
|
||||||
|
) -> error::JsonResult<serde_json::Value> {
|
||||||
|
let payload_r = run_query
|
||||||
|
.payload
|
||||||
|
.map(decode_payload)
|
||||||
|
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
|
||||||
|
|
||||||
|
let args = if let Some(payload) = payload_r {
|
||||||
|
payload?
|
||||||
|
} else {
|
||||||
|
serde_json::Map::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
|
||||||
|
let script_path = script_path.to_path();
|
||||||
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
|
||||||
|
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
|
||||||
|
|
||||||
|
let (uuid, tx) = push(
|
||||||
|
tx,
|
||||||
|
&w_id,
|
||||||
|
job_payload,
|
||||||
|
args,
|
||||||
|
&authed.username,
|
||||||
|
&authed.email,
|
||||||
|
username_to_permissioned_as(&authed.username),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
run_query.parent_job,
|
||||||
|
run_query.parent_job,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
!run_query.invisible_to_owner.unwrap_or(false),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
run_wait_result(
|
||||||
|
authed,
|
||||||
|
Extension(user_db),
|
||||||
|
*TIMEOUT_WAIT_RESULT,
|
||||||
|
uuid,
|
||||||
|
Path((w_id, script_path)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_wait_result_job_by_path(
|
pub async fn run_wait_result_job_by_path(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Path((w_id, script_path)): Path<(String, StripPath)>,
|
Path((w_id, script_path)): Path<(String, StripPath)>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
@@ -1341,9 +1485,8 @@ pub async fn run_wait_result_job_by_path(
|
|||||||
) -> error::JsonResult<serde_json::Value> {
|
) -> error::JsonResult<serde_json::Value> {
|
||||||
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
|
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
|
||||||
let script_path = script_path.to_path();
|
let script_path = script_path.to_path();
|
||||||
let mut tx = user_db.clone().begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
|
||||||
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
|
let job_payload = script_path_to_payload(script_path, tx.transaction_mut(), &w_id).await?;
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
|
||||||
|
|
||||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||||
|
|
||||||
@@ -1355,7 +1498,7 @@ pub async fn run_wait_result_job_by_path(
|
|||||||
&authed.username,
|
&authed.username,
|
||||||
&authed.email,
|
&authed.email,
|
||||||
username_to_permissioned_as(&authed.username),
|
username_to_permissioned_as(&authed.username),
|
||||||
scheduled_for,
|
None,
|
||||||
None,
|
None,
|
||||||
run_query.parent_job,
|
run_query.parent_job,
|
||||||
run_query.parent_job,
|
run_query.parent_job,
|
||||||
@@ -1380,6 +1523,7 @@ pub async fn run_wait_result_job_by_path(
|
|||||||
pub async fn run_wait_result_job_by_hash(
|
pub async fn run_wait_result_job_by_hash(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
|
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
@@ -1389,9 +1533,9 @@ pub async fn run_wait_result_job_by_hash(
|
|||||||
check_queue_too_long(db, run_query.queue_limit).await?;
|
check_queue_too_long(db, run_query.queue_limit).await?;
|
||||||
|
|
||||||
let hash = script_hash.0;
|
let hash = script_hash.0;
|
||||||
let mut tx = user_db.clone().begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
|
||||||
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
|
let path = get_path_for_hash(tx.transaction_mut(), &w_id, hash).await?;
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
|
||||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1402,7 +1546,7 @@ pub async fn run_wait_result_job_by_hash(
|
|||||||
&authed.username,
|
&authed.username,
|
||||||
&authed.email,
|
&authed.email,
|
||||||
username_to_permissioned_as(&authed.username),
|
username_to_permissioned_as(&authed.username),
|
||||||
scheduled_for,
|
None,
|
||||||
None,
|
None,
|
||||||
run_query.parent_job,
|
run_query.parent_job,
|
||||||
run_query.parent_job,
|
run_query.parent_job,
|
||||||
@@ -1427,6 +1571,7 @@ pub async fn run_wait_result_job_by_hash(
|
|||||||
pub async fn run_wait_result_flow_by_path(
|
pub async fn run_wait_result_flow_by_path(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
@@ -1436,8 +1581,8 @@ pub async fn run_wait_result_flow_by_path(
|
|||||||
check_queue_too_long(db, run_query.queue_limit).await?;
|
check_queue_too_long(db, run_query.queue_limit).await?;
|
||||||
|
|
||||||
let flow_path = flow_path.to_path();
|
let flow_path = flow_path.to_path();
|
||||||
let mut tx = user_db.clone().begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.clone().begin(&authed).await?).into();
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1458,7 +1603,6 @@ pub async fn run_wait_result_flow_by_path(
|
|||||||
!run_query.invisible_to_owner.unwrap_or(false),
|
!run_query.invisible_to_owner.unwrap_or(false),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
run_wait_result(
|
run_wait_result(
|
||||||
@@ -1489,13 +1633,14 @@ pub async fn script_path_to_payload<'c>(
|
|||||||
async fn run_preview_job(
|
async fn run_preview_job(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(preview): Json<Preview>,
|
Json(preview): Json<Preview>,
|
||||||
) -> error::Result<(StatusCode, String)> {
|
) -> error::Result<(StatusCode, String)> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||||
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1522,19 +1667,21 @@ async fn run_preview_job(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_preview_flow_job(
|
async fn run_preview_flow_job(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(raw_flow): Json<PreviewFlow>,
|
Json(raw_flow): Json<PreviewFlow>,
|
||||||
) -> error::Result<(StatusCode, String)> {
|
) -> error::Result<(StatusCode, String)> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||||
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1556,21 +1703,23 @@ async fn run_preview_flow_job(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_job_by_hash(
|
pub async fn run_job_by_hash(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
|
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
|
||||||
Query(run_query): Query<RunJobQuery>,
|
Query(run_query): Query<RunJobQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
||||||
) -> error::Result<(StatusCode, String)> {
|
) -> error::Result<(StatusCode, String)> {
|
||||||
let hash = script_hash.0;
|
let hash = script_hash.0;
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
|
let path = get_path_for_hash(tx.transaction_mut(), &w_id, hash).await?;
|
||||||
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
|
let scheduled_for = run_query.get_scheduled_for(tx.transaction_mut()).await?;
|
||||||
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
|
||||||
|
|
||||||
let (uuid, tx) = push(
|
let (uuid, tx) = push(
|
||||||
@@ -1592,6 +1741,7 @@ pub async fn run_job_by_hash(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1687,11 +1837,11 @@ fn list_completed_jobs_query(
|
|||||||
if let Some(pj) = &lq.parent_job {
|
if let Some(pj) = &lq.parent_job {
|
||||||
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
||||||
}
|
}
|
||||||
if let Some(dt) = &lq.created_before {
|
if let Some(dt) = &lq.started_before {
|
||||||
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||||
}
|
}
|
||||||
if let Some(dt) = &lq.created_after {
|
if let Some(dt) = &lq.started_after {
|
||||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||||
}
|
}
|
||||||
if let Some(sk) = &lq.is_skipped {
|
if let Some(sk) = &lq.is_skipped {
|
||||||
sqlb.and_where_eq("is_skipped", sk);
|
sqlb.and_where_eq("is_skipped", sk);
|
||||||
@@ -1706,6 +1856,14 @@ fn list_completed_jobs_query(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(args) = &lq.args {
|
||||||
|
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = &lq.result {
|
||||||
|
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
|
||||||
|
}
|
||||||
|
|
||||||
sqlb
|
sqlb
|
||||||
}
|
}
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
@@ -1714,8 +1872,8 @@ pub struct ListCompletedQuery {
|
|||||||
pub script_path_exact: Option<String>,
|
pub script_path_exact: Option<String>,
|
||||||
pub script_hash: Option<String>,
|
pub script_hash: Option<String>,
|
||||||
pub created_by: Option<String>,
|
pub created_by: Option<String>,
|
||||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
pub success: Option<bool>,
|
pub success: Option<bool>,
|
||||||
pub parent_job: Option<String>,
|
pub parent_job: Option<String>,
|
||||||
pub order_desc: Option<bool>,
|
pub order_desc: Option<bool>,
|
||||||
@@ -1723,6 +1881,10 @@ pub struct ListCompletedQuery {
|
|||||||
pub is_skipped: Option<bool>,
|
pub is_skipped: Option<bool>,
|
||||||
pub is_flow_step: Option<bool>,
|
pub is_flow_step: Option<bool>,
|
||||||
pub suspended: Option<bool>,
|
pub suspended: Option<bool>,
|
||||||
|
// filter by matching a subset of the args using base64 encoded json subset
|
||||||
|
pub args: Option<String>,
|
||||||
|
// filter by matching a subset of the result using base64 encoded json subset
|
||||||
|
pub result: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_completed_jobs(
|
async fn list_completed_jobs(
|
||||||
|
|||||||
@@ -7,17 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::oauth2::AllClients;
|
use crate::oauth2::AllClients;
|
||||||
use argon2::Argon2;
|
|
||||||
use axum::{middleware::from_extractor, routing::get, Extension, Router};
|
|
||||||
use db::DB;
|
|
||||||
use git_version::git_version;
|
|
||||||
use reqwest::Client;
|
|
||||||
use std::{net::SocketAddr, sync::Arc};
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_cookies::CookieManagerLayer;
|
|
||||||
use tower_http::trace::TraceLayer;
|
|
||||||
use windmill_common::utils::rd_string;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::UserDB,
|
db::UserDB,
|
||||||
oauth2::{build_oauth_clients, SlackVerifier},
|
oauth2::{build_oauth_clients, SlackVerifier},
|
||||||
@@ -25,6 +14,20 @@ use crate::{
|
|||||||
users::{Authed, OptAuthed},
|
users::{Authed, OptAuthed},
|
||||||
webhook_util::WebhookShared,
|
webhook_util::WebhookShared,
|
||||||
};
|
};
|
||||||
|
use argon2::Argon2;
|
||||||
|
use axum::{middleware::from_extractor, routing::get, Extension, Router};
|
||||||
|
use db::DB;
|
||||||
|
use git_version::git_version;
|
||||||
|
use hyper::Method;
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_cookies::CookieManagerLayer;
|
||||||
|
use tower_http::{
|
||||||
|
cors::{Any, CorsLayer},
|
||||||
|
trace::TraceLayer,
|
||||||
|
};
|
||||||
|
use windmill_common::utils::rd_string;
|
||||||
|
|
||||||
mod apps;
|
mod apps;
|
||||||
mod audit;
|
mod audit;
|
||||||
@@ -35,6 +38,7 @@ mod flows;
|
|||||||
mod folders;
|
mod folders;
|
||||||
mod granular_acls;
|
mod granular_acls;
|
||||||
mod groups;
|
mod groups;
|
||||||
|
mod inputs;
|
||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
mod oauth2;
|
mod oauth2;
|
||||||
mod resources;
|
mod resources;
|
||||||
@@ -77,6 +81,7 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
pub async fn run_server(
|
pub async fn run_server(
|
||||||
db: DB,
|
db: DB,
|
||||||
|
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@@ -96,10 +101,16 @@ pub async fn run_server(
|
|||||||
.on_request(()),
|
.on_request(()),
|
||||||
)
|
)
|
||||||
.layer(Extension(db.clone()))
|
.layer(Extension(db.clone()))
|
||||||
|
.layer(Extension(rsmq))
|
||||||
.layer(Extension(user_db))
|
.layer(Extension(user_db))
|
||||||
.layer(Extension(auth_cache.clone()))
|
.layer(Extension(auth_cache.clone()))
|
||||||
.layer(CookieManagerLayer::new())
|
.layer(CookieManagerLayer::new())
|
||||||
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
|
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET, Method::POST])
|
||||||
|
.allow_origin(Any);
|
||||||
|
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest(
|
.nest(
|
||||||
@@ -108,25 +119,27 @@ pub async fn run_server(
|
|||||||
.nest(
|
.nest(
|
||||||
"/w/:workspace_id",
|
"/w/:workspace_id",
|
||||||
Router::new()
|
Router::new()
|
||||||
|
// Reordered alphabetically
|
||||||
|
.nest("/acls", granular_acls::workspaced_service())
|
||||||
|
.nest("/apps", apps::workspaced_service())
|
||||||
|
.nest("/audit", audit::workspaced_service())
|
||||||
|
.nest("/capture", capture::workspaced_service())
|
||||||
|
.nest("/favorites", favorite::workspaced_service())
|
||||||
|
.nest("/flows", flows::workspaced_service())
|
||||||
|
.nest("/folders", folders::workspaced_service())
|
||||||
|
.nest("/groups", groups::workspaced_service())
|
||||||
|
.nest("/inputs", inputs::workspaced_service())
|
||||||
|
.nest("/jobs", jobs::workspaced_service().layer(cors.clone()))
|
||||||
|
.nest("/oauth", oauth2::workspaced_service())
|
||||||
|
.nest("/resources", resources::workspaced_service())
|
||||||
|
.nest("/schedules", schedule::workspaced_service())
|
||||||
.nest("/scripts", scripts::workspaced_service())
|
.nest("/scripts", scripts::workspaced_service())
|
||||||
.nest("/jobs", jobs::workspaced_service())
|
|
||||||
.nest(
|
.nest(
|
||||||
"/users",
|
"/users",
|
||||||
users::workspaced_service().layer(Extension(argon2.clone())),
|
users::workspaced_service().layer(Extension(argon2.clone())),
|
||||||
)
|
)
|
||||||
.nest("/variables", variables::workspaced_service())
|
.nest("/variables", variables::workspaced_service())
|
||||||
.nest("/oauth", oauth2::workspaced_service())
|
.nest("/workspaces", workspaces::workspaced_service()),
|
||||||
.nest("/resources", resources::workspaced_service())
|
|
||||||
.nest("/schedules", schedule::workspaced_service())
|
|
||||||
.nest("/groups", groups::workspaced_service())
|
|
||||||
.nest("/audit", audit::workspaced_service())
|
|
||||||
.nest("/acls", granular_acls::workspaced_service())
|
|
||||||
.nest("/workspaces", workspaces::workspaced_service())
|
|
||||||
.nest("/apps", apps::workspaced_service())
|
|
||||||
.nest("/flows", flows::workspaced_service())
|
|
||||||
.nest("/capture", capture::workspaced_service())
|
|
||||||
.nest("/favorites", favorite::workspaced_service())
|
|
||||||
.nest("/folders", folders::workspaced_service()),
|
|
||||||
)
|
)
|
||||||
.nest("/workspaces", workspaces::global_service())
|
.nest("/workspaces", workspaces::global_service())
|
||||||
.nest(
|
.nest(
|
||||||
@@ -136,16 +149,25 @@ pub async fn run_server(
|
|||||||
.nest("/workers", worker_ping::global_service())
|
.nest("/workers", worker_ping::global_service())
|
||||||
.nest("/scripts", scripts::global_service())
|
.nest("/scripts", scripts::global_service())
|
||||||
.nest("/flows", flows::global_service())
|
.nest("/flows", flows::global_service())
|
||||||
.nest("/apps", apps::global_service())
|
.nest("/apps", apps::global_service().layer(cors.clone()))
|
||||||
.nest("/schedules", schedule::global_service())
|
.nest("/schedules", schedule::global_service())
|
||||||
.route_layer(from_extractor::<Authed>())
|
.route_layer(from_extractor::<Authed>())
|
||||||
.route_layer(from_extractor::<users::Tokened>())
|
.route_layer(from_extractor::<users::Tokened>())
|
||||||
|
.nest("/scripts_u", scripts::global_unauthed_service())
|
||||||
.nest(
|
.nest(
|
||||||
"/w/:workspace_id/apps_u",
|
"/w/:workspace_id/apps_u",
|
||||||
apps::unauthed_service().layer(from_extractor::<OptAuthed>()),
|
apps::unauthed_service()
|
||||||
|
.layer(from_extractor::<OptAuthed>())
|
||||||
|
.layer(cors.clone()),
|
||||||
|
)
|
||||||
|
.nest(
|
||||||
|
"/w/:workspace_id/jobs_u",
|
||||||
|
jobs::global_service().layer(cors.clone()),
|
||||||
|
)
|
||||||
|
.nest(
|
||||||
|
"/w/:workspace_id/capture_u",
|
||||||
|
capture::global_service().layer(cors),
|
||||||
)
|
)
|
||||||
.nest("/w/:workspace_id/jobs_u", jobs::global_service())
|
|
||||||
.nest("/w/:workspace_id/capture_u", capture::global_service())
|
|
||||||
.nest(
|
.nest(
|
||||||
"/auth",
|
"/auth",
|
||||||
users::make_unauthed_service().layer(Extension(argon2)),
|
users::make_unauthed_service().layer(Extension(argon2)),
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ use windmill_audit::{audit_log, ActionKind};
|
|||||||
use windmill_common::users::username_to_permissioned_as;
|
use windmill_common::users::username_to_permissioned_as;
|
||||||
use windmill_common::utils::{not_found_if_none, now_from_db};
|
use windmill_common::utils::{not_found_if_none, now_from_db};
|
||||||
|
|
||||||
use crate::users::{truncate_token, Authed, NEW_USER_WEBHOOK};
|
use crate::users::{truncate_token, Authed};
|
||||||
|
use crate::webhook_util::{InstanceEvent, WebhookShared};
|
||||||
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
|
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
@@ -43,7 +44,7 @@ use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRE
|
|||||||
use windmill_common::error::{self, to_anyhow, Error};
|
use windmill_common::error::{self, to_anyhow, Error};
|
||||||
use windmill_common::oauth2::*;
|
use windmill_common::oauth2::*;
|
||||||
|
|
||||||
use windmill_queue::JobPayload;
|
use windmill_queue::{JobPayload, QueueTransaction};
|
||||||
|
|
||||||
use std::{fs, str};
|
use std::{fs, str};
|
||||||
|
|
||||||
@@ -728,6 +729,7 @@ where
|
|||||||
async fn slack_command(
|
async fn slack_command(
|
||||||
SlackSig { sig, ts }: SlackSig,
|
SlackSig { sig, ts }: SlackSig,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
body: Bytes,
|
body: Bytes,
|
||||||
) -> error::Result<String> {
|
) -> error::Result<String> {
|
||||||
let form: SlackCommand = serde_urlencoded::from_bytes(&body)
|
let form: SlackCommand = serde_urlencoded::from_bytes(&body)
|
||||||
@@ -740,7 +742,7 @@ async fn slack_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tx = db.begin().await?;
|
let mut tx: QueueTransaction<'_, _> = (rsmq, db.begin().await?).into();
|
||||||
let settings = sqlx::query_as!(
|
let settings = sqlx::query_as!(
|
||||||
WorkspaceSettings,
|
WorkspaceSettings,
|
||||||
"SELECT * FROM workspace_settings WHERE slack_team_id = $1",
|
"SELECT * FROM workspace_settings WHERE slack_team_id = $1",
|
||||||
@@ -756,7 +758,7 @@ async fn slack_command(
|
|||||||
} else {
|
} else {
|
||||||
let path = path.strip_prefix("script/").unwrap_or_else(|| path);
|
let path = path.strip_prefix("script/").unwrap_or_else(|| path);
|
||||||
let script_hash = windmill_common::get_latest_hash_for_path(
|
let script_hash = windmill_common::get_latest_hash_for_path(
|
||||||
&mut tx,
|
tx.transaction_mut(),
|
||||||
&settings.workspace_id,
|
&settings.workspace_id,
|
||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
@@ -788,14 +790,15 @@ async fn slack_command(
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
|
||||||
let url = BASE_URL.to_owned();
|
let url = BASE_URL.to_owned();
|
||||||
|
tx.commit().await?;
|
||||||
return Ok(format!(
|
return Ok(format!(
|
||||||
"Job launched. See details at {url}/run/{uuid}?workspace={}",
|
"Job launched. See details at {url}/run/{uuid}?workspace={}",
|
||||||
&settings.workspace_id
|
&settings.workspace_id
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
return Ok(format!(
|
return Ok(format!(
|
||||||
"workspace not properly configured (did you set the script to trigger in the settings?)"
|
"workspace not properly configured (did you set the script to trigger in the settings?)"
|
||||||
@@ -815,6 +818,7 @@ async fn login_callback(
|
|||||||
Path(client_name): Path<String>,
|
Path(client_name): Path<String>,
|
||||||
cookies: Cookies,
|
cookies: Cookies,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Json(callback): Json<OAuthCallback>,
|
Json(callback): Json<OAuthCallback>,
|
||||||
) -> error::Result<String> {
|
) -> error::Result<String> {
|
||||||
let client_w_config = &OAUTH_CLIENTS
|
let client_w_config = &OAUTH_CLIENTS
|
||||||
@@ -849,7 +853,8 @@ async fn login_callback(
|
|||||||
_ => user.email.ok_or_else(|| {
|
_ => user.email.ok_or_else(|| {
|
||||||
error::Error::BadRequest("email address not fetchable from user info".to_string())
|
error::Error::BadRequest("email address not fetchable from user info".to_string())
|
||||||
})?,
|
})?,
|
||||||
};
|
}
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
if let Some(domains) = &client_w_config.allowed_domains {
|
if let Some(domains) = &client_w_config.allowed_domains {
|
||||||
if !domains.iter().any(|d| email.ends_with(d)) {
|
if !domains.iter().any(|d| email.ends_with(d)) {
|
||||||
@@ -939,14 +944,7 @@ async fn login_callback(
|
|||||||
}
|
}
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
webhook.send_instance_event(InstanceEvent::UserSignupOAuth { email: email.clone() });
|
||||||
let _ = HTTP_CLIENT
|
|
||||||
.post(&new_user_webhook)
|
|
||||||
.json(&serde_json::json!({"email" : &email, "event": "oauth_signup"}))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok("Successfully logged in".to_string())
|
Ok("Successfully logged in".to_string())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
users::{require_owner_of_path, Authed},
|
users::{maybe_refresh_folders, require_owner_of_path, Authed},
|
||||||
webhook_util::{WebhookMessage, WebhookShared},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -264,9 +264,12 @@ async fn create_resource(
|
|||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(resource): Json<CreateResource>,
|
Json(resource): Json<CreateResource>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
|
let authed = maybe_refresh_folders(&resource.path, &w_id, authed, &db).await;
|
||||||
|
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
check_path_conflict(&mut tx, &w_id, &resource.path).await?;
|
check_path_conflict(&mut tx, &w_id, &resource.path).await?;
|
||||||
@@ -375,6 +378,7 @@ async fn update_resource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sqlb.returning("path");
|
sqlb.returning("path");
|
||||||
|
let authed = maybe_refresh_folders(path, &w_id, authed, &db).await;
|
||||||
|
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -6,27 +6,26 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
users::Authed,
|
users::{maybe_refresh_folders, Authed},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, Path, Query},
|
extract::{Extension, Path, Query},
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{Postgres, Transaction};
|
use sqlx::{Postgres, Transaction};
|
||||||
|
use std::str::FromStr;
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{Error, JsonResult, Result},
|
error::{Error, JsonResult, Result},
|
||||||
schedule::Schedule,
|
schedule::Schedule,
|
||||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||||
};
|
};
|
||||||
use windmill_queue::{self, schedule::push_scheduled_job, JobKind};
|
use windmill_queue::{self, schedule::push_scheduled_job, JobKind, QueueTransaction};
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -47,7 +46,7 @@ pub fn global_service() -> Router {
|
|||||||
pub struct NewSchedule {
|
pub struct NewSchedule {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub schedule: String,
|
pub schedule: String,
|
||||||
pub offset: i32,
|
pub timezone: String,
|
||||||
pub script_path: String,
|
pub script_path: String,
|
||||||
pub is_flow: bool,
|
pub is_flow: bool,
|
||||||
pub args: Option<serde_json::Value>,
|
pub args: Option<serde_json::Value>,
|
||||||
@@ -78,23 +77,34 @@ async fn check_path_conflict<'c>(
|
|||||||
|
|
||||||
async fn create_schedule(
|
async fn create_schedule(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(ns): Json<NewSchedule>,
|
Json(ns): Json<NewSchedule>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
|
||||||
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
|
|
||||||
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
|
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||||
check_path_conflict(&mut tx, &w_id, &ns.path).await?;
|
check_path_conflict(tx.transaction_mut(), &w_id, &ns.path).await?;
|
||||||
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
|
check_flow_conflict(
|
||||||
|
tx.transaction_mut(),
|
||||||
|
&w_id,
|
||||||
|
&ns.path,
|
||||||
|
ns.is_flow,
|
||||||
|
&ns.script_path,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let schedule = sqlx::query_as!(
|
let schedule = sqlx::query_as!(
|
||||||
Schedule,
|
Schedule,
|
||||||
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \
|
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
|
||||||
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
|
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
|
||||||
w_id,
|
w_id,
|
||||||
ns.path,
|
ns.path,
|
||||||
ns.schedule,
|
ns.schedule,
|
||||||
ns.offset,
|
ns.timezone,
|
||||||
&authed.username,
|
&authed.username,
|
||||||
ns.script_path,
|
ns.script_path,
|
||||||
ns.is_flow,
|
ns.is_flow,
|
||||||
@@ -135,13 +145,18 @@ async fn create_schedule(
|
|||||||
|
|
||||||
async fn edit_schedule(
|
async fn edit_schedule(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, path)): Path<(String, StripPath)>,
|
Path((w_id, path)): Path<(String, StripPath)>,
|
||||||
Json(es): Json<EditSchedule>,
|
Json(es): Json<EditSchedule>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
|
||||||
let path = path.to_path();
|
let path = path.to_path();
|
||||||
|
|
||||||
|
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
|
||||||
|
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
|
||||||
|
(rsmq, user_db.begin(&authed).await?).into();
|
||||||
|
|
||||||
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
|
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||||
|
|
||||||
let is_flow = sqlx::query_scalar!(
|
let is_flow = sqlx::query_scalar!(
|
||||||
@@ -152,12 +167,13 @@ async fn edit_schedule(
|
|||||||
.fetch_one(&mut tx)
|
.fetch_one(&mut tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
clear_schedule(&mut tx, path, is_flow).await?;
|
clear_schedule(tx.transaction_mut(), path, is_flow).await?;
|
||||||
let schedule = sqlx::query_as!(
|
let schedule = sqlx::query_as!(
|
||||||
Schedule,
|
Schedule,
|
||||||
"UPDATE schedule SET schedule = $1, args = $2 WHERE path \
|
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path \
|
||||||
= $3 AND workspace_id = $4 RETURNING *",
|
= $4 AND workspace_id = $5 RETURNING *",
|
||||||
es.schedule,
|
es.schedule,
|
||||||
|
es.timezone,
|
||||||
es.args,
|
es.args,
|
||||||
path,
|
path,
|
||||||
w_id,
|
w_id,
|
||||||
@@ -166,10 +182,6 @@ async fn edit_schedule(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
|
||||||
|
|
||||||
if schedule.enabled {
|
|
||||||
tx = push_scheduled_job(tx, schedule).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
audit_log(
|
audit_log(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&authed.username,
|
&authed.username,
|
||||||
@@ -185,6 +197,10 @@ async fn edit_schedule(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if schedule.enabled {
|
||||||
|
tx = push_scheduled_job(tx, schedule).await?;
|
||||||
|
}
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(path.to_string())
|
Ok(path.to_string())
|
||||||
@@ -235,15 +251,26 @@ async fn exists_schedule(
|
|||||||
Ok(Json(res))
|
Ok(Json(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PreviewPayload {
|
||||||
|
pub schedule: String,
|
||||||
|
pub timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn preview_schedule(
|
pub async fn preview_schedule(
|
||||||
Json(payload): Json<PreviewPayload>,
|
Json(payload): Json<PreviewPayload>,
|
||||||
) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
|
) -> JsonResult<Vec<DateTime<Utc>>> {
|
||||||
let schedule = cron::Schedule::from_str(&payload.schedule)
|
let schedule = cron::Schedule::from_str(&payload.schedule)
|
||||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||||
let upcoming: Vec<DateTime<chrono::Utc>> = schedule
|
|
||||||
.upcoming(get_offset(payload.offset))
|
let tz =
|
||||||
.take(10)
|
chrono_tz::Tz::from_str(&payload.timezone).map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||||
.map(|x| x.into())
|
|
||||||
|
let upcoming: Vec<DateTime<Utc>> = schedule
|
||||||
|
.upcoming(tz)
|
||||||
|
.take(5)
|
||||||
|
// Convert back to UTC for a standardised API response. The client will convert to the local timezone.
|
||||||
|
.map(|x| x.with_timezone(&Utc))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Json(upcoming))
|
Ok(Json(upcoming))
|
||||||
@@ -252,10 +279,12 @@ pub async fn preview_schedule(
|
|||||||
pub async fn set_enabled(
|
pub async fn set_enabled(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Path((w_id, path)): Path<(String, StripPath)>,
|
Path((w_id, path)): Path<(String, StripPath)>,
|
||||||
Json(payload): Json<SetEnabled>,
|
Json(payload): Json<SetEnabled>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx: QueueTransaction<'_, rsmq_async::MultiplexedRsmq> =
|
||||||
|
(rsmq, user_db.begin(&authed).await?).into();
|
||||||
let path = path.to_path();
|
let path = path.to_path();
|
||||||
let schedule_o = sqlx::query_as!(
|
let schedule_o = sqlx::query_as!(
|
||||||
Schedule,
|
Schedule,
|
||||||
@@ -270,11 +299,8 @@ pub async fn set_enabled(
|
|||||||
|
|
||||||
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
|
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
|
||||||
|
|
||||||
clear_schedule(&mut tx, path, schedule.is_flow).await?;
|
clear_schedule(tx.transaction_mut(), path, schedule.is_flow).await?;
|
||||||
|
|
||||||
if payload.enabled {
|
|
||||||
tx = push_scheduled_job(tx, schedule).await?;
|
|
||||||
}
|
|
||||||
audit_log(
|
audit_log(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&authed.username,
|
&authed.username,
|
||||||
@@ -285,7 +311,12 @@ pub async fn set_enabled(
|
|||||||
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
|
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if payload.enabled {
|
||||||
|
tx = push_scheduled_job(tx, schedule).await?;
|
||||||
|
}
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"succesfully updated schedule at path {} to status {}",
|
"succesfully updated schedule at path {} to status {}",
|
||||||
path, payload.enabled
|
path, payload.enabled
|
||||||
@@ -353,6 +384,7 @@ async fn check_flow_conflict<'c>(
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct EditSchedule {
|
pub struct EditSchedule {
|
||||||
pub schedule: String,
|
pub schedule: String,
|
||||||
|
pub timezone: String,
|
||||||
pub args: Option<serde_json::Value>,
|
pub args: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,16 +408,6 @@ pub async fn clear_schedule<'c>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct PreviewPayload {
|
|
||||||
pub schedule: String,
|
|
||||||
pub offset: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_offset(offset: Option<i32>) -> FixedOffset {
|
|
||||||
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct SetEnabled {
|
pub struct SetEnabled {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|||||||
@@ -6,13 +6,10 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use sql_builder::prelude::*;
|
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
schedule::clear_schedule,
|
schedule::clear_schedule,
|
||||||
users::{require_owner_of_path, Authed},
|
users::{maybe_refresh_folders, require_owner_of_path, AuthCache, Authed},
|
||||||
webhook_util::{WebhookMessage, WebhookShared},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
HTTP_CLIENT,
|
HTTP_CLIENT,
|
||||||
};
|
};
|
||||||
@@ -24,12 +21,15 @@ use axum::{
|
|||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use sql_builder::prelude::*;
|
||||||
use sql_builder::SqlBuilder;
|
use sql_builder::SqlBuilder;
|
||||||
use sqlx::{FromRow, Postgres, Transaction};
|
use sqlx::{FromRow, Postgres, Transaction};
|
||||||
use std::{
|
use std::{
|
||||||
collections::hash_map::DefaultHasher,
|
collections::hash_map::DefaultHasher,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{Error, JsonResult, Result},
|
error::{Error, JsonResult, Result},
|
||||||
schedule::Schedule,
|
schedule::Schedule,
|
||||||
@@ -42,7 +42,8 @@ use windmill_common::{
|
|||||||
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
|
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use windmill_queue::{self, schedule::push_scheduled_job};
|
use windmill_parser::MainArgSignature;
|
||||||
|
use windmill_queue::{self, schedule::push_scheduled_job, QueueTransaction};
|
||||||
|
|
||||||
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
|
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
|
||||||
|
|
||||||
@@ -60,6 +61,13 @@ pub fn global_service() -> Router {
|
|||||||
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
|
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn global_unauthed_service() -> Router {
|
||||||
|
Router::new().route(
|
||||||
|
"/tokened_raw/:workspace/:token/*path",
|
||||||
|
get(get_tokened_raw_script_by_path),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn workspaced_service() -> Router {
|
pub fn workspaced_service() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/list", get(list_scripts))
|
.route("/list", get(list_scripts))
|
||||||
@@ -76,6 +84,7 @@ pub fn workspaced_service() -> Router {
|
|||||||
.route("/deployment_status/h/:hash", get(get_deployment_status))
|
.route("/deployment_status/h/:hash", get(get_deployment_status))
|
||||||
.route("/list_paths", get(list_paths))
|
.route("/list_paths", get(list_paths))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_scripts(
|
async fn list_scripts(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
@@ -184,20 +193,22 @@ fn hash_script(ns: &NewScript) -> i64 {
|
|||||||
async fn create_script(
|
async fn create_script(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(ns): Json<NewScript>,
|
Json(ns): Json<NewScript>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
let hash = ScriptHash(hash_script(&ns));
|
let hash = ScriptHash(hash_script(&ns));
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
|
||||||
|
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
|
||||||
|
|
||||||
if sqlx::query_scalar!(
|
if sqlx::query_scalar!(
|
||||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||||
hash.0,
|
hash.0,
|
||||||
&w_id
|
&w_id
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut tx)
|
.fetch_optional(tx.transaction_mut())
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
@@ -258,7 +269,7 @@ async fn create_script(
|
|||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
|
let ps = get_script_by_hash_internal(tx.transaction_mut(), &w_id, p_hash).await?;
|
||||||
|
|
||||||
if ps.path != ns.path {
|
if ps.path != ns.path {
|
||||||
if !authed.is_admin {
|
if !authed.is_admin {
|
||||||
@@ -353,7 +364,7 @@ async fn create_script(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for schedule in schedulables {
|
for schedule in schedulables {
|
||||||
clear_schedule(&mut tx, &schedule.path, false).await?;
|
clear_schedule(tx.transaction_mut(), &schedule.path, false).await?;
|
||||||
|
|
||||||
if schedule.enabled {
|
if schedule.enabled {
|
||||||
tx = push_scheduled_job(tx, schedule).await?;
|
tx = push_scheduled_job(tx, schedule).await?;
|
||||||
@@ -361,36 +372,6 @@ async fn create_script(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tx = if needs_lock_gen {
|
|
||||||
let dependencies = match ns.language {
|
|
||||||
ScriptLang::Python3 => {
|
|
||||||
windmill_parser_py::parse_python_imports(&ns.content)?.join("\n")
|
|
||||||
}
|
|
||||||
_ => ns.content,
|
|
||||||
};
|
|
||||||
let (_, tx) = windmill_queue::push(
|
|
||||||
tx,
|
|
||||||
&w_id,
|
|
||||||
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
|
|
||||||
serde_json::Map::new(),
|
|
||||||
&authed.username,
|
|
||||||
&authed.email,
|
|
||||||
username_to_permissioned_as(&authed.username),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
tx
|
|
||||||
} else {
|
|
||||||
tx
|
|
||||||
};
|
|
||||||
|
|
||||||
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
|
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {
|
||||||
audit_log(
|
audit_log(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
@@ -405,7 +386,7 @@ async fn create_script(
|
|||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::UpdateScript {
|
WebhookMessage::UpdateScript {
|
||||||
workspace: w_id,
|
workspace: w_id.clone(),
|
||||||
path: ns.path.clone(),
|
path: ns.path.clone(),
|
||||||
hash: hash.to_string(),
|
hash: hash.to_string(),
|
||||||
},
|
},
|
||||||
@@ -430,13 +411,41 @@ async fn create_script(
|
|||||||
webhook.send_message(
|
webhook.send_message(
|
||||||
w_id.clone(),
|
w_id.clone(),
|
||||||
WebhookMessage::CreateScript {
|
WebhookMessage::CreateScript {
|
||||||
workspace: w_id,
|
workspace: w_id.clone(),
|
||||||
path: ns.path.clone(),
|
path: ns.path.clone(),
|
||||||
hash: hash.to_string(),
|
hash: hash.to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if needs_lock_gen {
|
||||||
|
let dependencies = match ns.language {
|
||||||
|
ScriptLang::Python3 => {
|
||||||
|
windmill_parser_py::parse_python_imports(&ns.content)?.join("\n")
|
||||||
|
}
|
||||||
|
_ => ns.content,
|
||||||
|
};
|
||||||
|
let (_, new_tx) = windmill_queue::push(
|
||||||
|
tx,
|
||||||
|
&w_id,
|
||||||
|
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
|
||||||
|
serde_json::Map::new(),
|
||||||
|
&authed.username,
|
||||||
|
&authed.email,
|
||||||
|
username_to_permissioned_as(&authed.username),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tx = new_tx;
|
||||||
|
}
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok((StatusCode::CREATED, format!("{}", hash)))
|
Ok((StatusCode::CREATED, format!("{}", hash)))
|
||||||
@@ -496,6 +505,18 @@ async fn list_paths(
|
|||||||
Ok(Json(scripts))
|
Ok(Json(scripts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_tokened_raw_script_by_path(
|
||||||
|
Extension(user_db): Extension<UserDB>,
|
||||||
|
Path((w_id, token, path)): Path<(String, String, StripPath)>,
|
||||||
|
Extension(cache): Extension<Arc<AuthCache>>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let authed = cache
|
||||||
|
.get_authed(Some(w_id.clone()), &token)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| Error::NotAuthorized("Invalid token".to_string()))?;
|
||||||
|
return raw_script_by_path(authed, Extension(user_db), Path((w_id, path))).await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn raw_script_by_path(
|
async fn raw_script_by_path(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
@@ -556,11 +577,10 @@ async fn get_script_by_hash_internal<'c>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_script_by_hash(
|
async fn get_script_by_hash(
|
||||||
authed: Authed,
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
|
||||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||||
) -> JsonResult<Script> {
|
) -> JsonResult<Script> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = db.begin().await?;
|
||||||
let r = get_script_by_hash_internal(&mut tx, &w_id, &hash).await?;
|
let r = get_script_by_hash_internal(&mut tx, &w_id, &hash).await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
@@ -568,11 +588,10 @@ async fn get_script_by_hash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn raw_script_by_hash(
|
async fn raw_script_by_hash(
|
||||||
authed: Authed,
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
|
||||||
Path((w_id, hash_str)): Path<(String, String)>,
|
Path((w_id, hash_str)): Path<(String, String)>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = db.begin().await?;
|
||||||
let hash = ScriptHash(to_i64(hash_str.strip_suffix(".ts").ok_or_else(|| {
|
let hash = ScriptHash(to_i64(hash_str.strip_suffix(".ts").ok_or_else(|| {
|
||||||
Error::BadRequest("Raw script path must end with .ts".to_string())
|
Error::BadRequest("Raw script path must end with .ts".to_string())
|
||||||
})?)?);
|
})?)?);
|
||||||
@@ -588,11 +607,10 @@ struct DeploymentStatus {
|
|||||||
lock_error_logs: Option<String>,
|
lock_error_logs: Option<String>,
|
||||||
}
|
}
|
||||||
async fn get_deployment_status(
|
async fn get_deployment_status(
|
||||||
authed: Authed,
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
|
||||||
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
Path((w_id, hash)): Path<(String, ScriptHash)>,
|
||||||
) -> JsonResult<DeploymentStatus> {
|
) -> JsonResult<DeploymentStatus> {
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = db.begin().await?;
|
||||||
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
|
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
|
||||||
DeploymentStatus,
|
DeploymentStatus,
|
||||||
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
|
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||||
@@ -761,25 +779,31 @@ async fn delete_script_by_path(
|
|||||||
Ok(Json(script))
|
Ok(Json(script))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_python_code_to_jsonschema(
|
#[derive(Debug, Serialize)]
|
||||||
Json(code): Json<String>,
|
#[serde(tag = "type")]
|
||||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
enum SigParsing {
|
||||||
windmill_parser_py::parse_python_signature(&code).map(Json)
|
Valid(MainArgSignature),
|
||||||
|
Invalid { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_deno_code_to_jsonschema(
|
fn result_to_sig_parsing(result: Result<MainArgSignature>) -> Json<SigParsing> {
|
||||||
Json(code): Json<String>,
|
match result {
|
||||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
Ok(sig) => Json(SigParsing::Valid(sig)),
|
||||||
windmill_parser_ts::parse_deno_signature(&code).map(Json)
|
Err(e) => Json(SigParsing::Invalid { error: e.to_string() }),
|
||||||
}
|
}
|
||||||
async fn parse_go_code_to_jsonschema(
|
|
||||||
Json(code): Json<String>,
|
|
||||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
|
||||||
windmill_parser_go::parse_go_sig(&code).map(Json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_bash_code_to_jsonschema(
|
async fn parse_python_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
|
||||||
Json(code): Json<String>,
|
result_to_sig_parsing(windmill_parser_py::parse_python_signature(&code))
|
||||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
}
|
||||||
windmill_parser_bash::parse_bash_sig(&code).map(Json)
|
|
||||||
|
async fn parse_deno_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
|
||||||
|
result_to_sig_parsing(windmill_parser_ts::parse_deno_signature(&code, false))
|
||||||
|
}
|
||||||
|
async fn parse_go_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
|
||||||
|
result_to_sig_parsing(windmill_parser_go::parse_go_sig(&code))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn parse_bash_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
|
||||||
|
result_to_sig_parsing(windmill_parser_bash::parse_bash_sig(&code))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ use crate::{
|
|||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
folders::get_folders_for_user,
|
folders::get_folders_for_user,
|
||||||
utils::require_super_admin,
|
utils::require_super_admin,
|
||||||
|
webhook_util::{InstanceEvent, WebhookShared},
|
||||||
workspaces::invite_user_to_all_auto_invite_worspaces,
|
workspaces::invite_user_to_all_auto_invite_worspaces,
|
||||||
COOKIE_DOMAIN, HTTP_CLIENT, IS_SECURE,
|
COOKIE_DOMAIN, IS_SECURE,
|
||||||
};
|
};
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -25,7 +26,9 @@ use axum::{
|
|||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use hyper::{header::LOCATION, StatusCode};
|
use hyper::{header::LOCATION, StatusCode};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
use regex::Regex;
|
||||||
use retainer::Cache;
|
use retainer::Cache;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
@@ -35,6 +38,7 @@ use tracing::{Instrument, Span};
|
|||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{self, Error, JsonResult, Result},
|
error::{self, Error, JsonResult, Result},
|
||||||
|
users::SUPERADMIN_SECRET_EMAIL,
|
||||||
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
|
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
|
||||||
};
|
};
|
||||||
use windmill_queue::CLOUD_HOSTED;
|
use windmill_queue::CLOUD_HOSTED;
|
||||||
@@ -73,6 +77,7 @@ pub fn global_service() -> Router {
|
|||||||
.route("/tokens/create", post(create_token))
|
.route("/tokens/create", post(create_token))
|
||||||
.route("/tokens/delete/:token_prefix", delete(delete_token))
|
.route("/tokens/delete/:token_prefix", delete(delete_token))
|
||||||
.route("/tokens/list", get(list_tokens))
|
.route("/tokens/list", get(list_tokens))
|
||||||
|
.route("/tokens/impersonate", post(impersonate))
|
||||||
.route("/usage", get(get_usage))
|
.route("/usage", get(get_usage))
|
||||||
// .route("/list_invite_codes", get(list_invite_codes))
|
// .route("/list_invite_codes", get(list_invite_codes))
|
||||||
// .route("/create_invite_code", post(create_invite_code))
|
// .route("/create_invite_code", post(create_invite_code))
|
||||||
@@ -99,6 +104,10 @@ impl AuthCache {
|
|||||||
AuthCache { cache: Cache::new(), db, superadmin_secret }
|
AuthCache { cache: Cache::new(), db, superadmin_secret }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn invalidate(&self, w_id: &str, token: String) {
|
||||||
|
self.cache.remove(&(w_id.to_string(), token)).await;
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
|
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
|
||||||
let key = (
|
let key = (
|
||||||
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
|
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
|
||||||
@@ -266,7 +275,7 @@ impl AuthCache {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
Some(Authed {
|
Some(Authed {
|
||||||
email: "superadmin_secret@windmill.dev".to_string(),
|
email: SUPERADMIN_SECRET_EMAIL.to_string(),
|
||||||
username: "superadmin_secret".to_string(),
|
username: "superadmin_secret".to_string(),
|
||||||
is_admin: true,
|
is_admin: true,
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
@@ -353,6 +362,31 @@ pub struct Authed {
|
|||||||
pub folders: Vec<(String, bool)>,
|
pub folders: Vec<(String, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn maybe_refresh_folders(path: &str, w_id: &str, authed: Authed, db: &DB) -> Authed {
|
||||||
|
if authed.is_admin {
|
||||||
|
return authed;
|
||||||
|
}
|
||||||
|
let splitted = path.split('/').collect::<Vec<_>>();
|
||||||
|
if splitted.len() >= 2
|
||||||
|
&& splitted[0] == "f"
|
||||||
|
&& !authed.folders.iter().any(|(f, _)| f == splitted[1])
|
||||||
|
{
|
||||||
|
let name = &authed.username;
|
||||||
|
let groups = get_groups_for_user(w_id, name, db)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let folders = get_folders_for_user(w_id, name, &groups, db)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Authed { folders, ..authed }
|
||||||
|
} else {
|
||||||
|
authed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S> FromRequestParts<S> for Authed
|
impl<S> FromRequestParts<S> for Authed
|
||||||
where
|
where
|
||||||
@@ -561,6 +595,7 @@ pub struct TruncatedToken {
|
|||||||
pub struct NewToken {
|
pub struct NewToken {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub impersonate_email: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -997,14 +1032,22 @@ async fn decline_invite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref VALID_USERNAME: Regex = Regex::new(r#"^[a-zA-Z_0-9]+$"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
async fn accept_invite(
|
async fn accept_invite(
|
||||||
Authed { email, .. }: Authed,
|
Authed { email, .. }: Authed,
|
||||||
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Json(nu): Json<AcceptInvite>,
|
Json(nu): Json<AcceptInvite>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
if &nu.username == "bot" {
|
if !VALID_USERNAME.is_match(&nu.username) {
|
||||||
return Err(Error::BadRequest("bot is a reserved username".to_string()));
|
return Err(windmill_common::error::Error::BadRequest(format!(
|
||||||
|
"Usermame can only contain alphanumeric characters and underscores"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
|
|
||||||
let r = sqlx::query!(
|
let r = sqlx::query!(
|
||||||
@@ -1041,6 +1084,11 @@ async fn accept_invite(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_some {
|
if is_some {
|
||||||
|
webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace {
|
||||||
|
email: email.clone(),
|
||||||
|
workspace: nu.workspace_id.clone(),
|
||||||
|
username: nu.username.clone(),
|
||||||
|
});
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
format!(
|
format!(
|
||||||
@@ -1077,6 +1125,12 @@ async fn add_user_to_workspace<'c>(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !VALID_USERNAME.is_match(username) {
|
||||||
|
return Err(windmill_common::error::Error::BadRequest(format!(
|
||||||
|
"Usermame can only contain alphanumeric characters and underscores"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
let already_exists_email = sqlx::query_scalar!(
|
let already_exists_email = sqlx::query_scalar!(
|
||||||
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
|
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
|
||||||
&w_id,
|
&w_id,
|
||||||
@@ -1275,12 +1329,20 @@ lazy_static::lazy_static! {
|
|||||||
async fn create_user(
|
async fn create_user(
|
||||||
Authed { email, .. }: Authed,
|
Authed { email, .. }: Authed,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
||||||
Json(nu): Json<NewUser>,
|
Json(mut nu): Json<NewUser>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
|
|
||||||
require_super_admin(&mut tx, &email).await?;
|
require_super_admin(&mut tx, &email).await?;
|
||||||
|
nu.email = nu.email.to_lowercase();
|
||||||
|
|
||||||
|
if nu.email == SUPERADMIN_SECRET_EMAIL {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
"The superadmin email is a reserved email".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
|
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
|
||||||
@@ -1308,19 +1370,9 @@ async fn create_user(
|
|||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
|
||||||
let _ = HTTP_CLIENT
|
|
||||||
.post(&new_user_webhook)
|
|
||||||
.json(
|
|
||||||
&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "global_add"}),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
|
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
|
||||||
|
|
||||||
|
webhook.send_instance_event(InstanceEvent::UserAdded { email: nu.email.clone() });
|
||||||
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
|
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1566,7 +1618,7 @@ async fn login(
|
|||||||
Json(Login { email, password }): Json<Login>,
|
Json(Login { email, password }): Json<Login>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
|
let email = email.to_lowercase();
|
||||||
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as(
|
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as(
|
||||||
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \
|
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \
|
||||||
'password'",
|
'password'",
|
||||||
@@ -1688,6 +1740,58 @@ async fn create_token(
|
|||||||
Ok((StatusCode::CREATED, token))
|
Ok((StatusCode::CREATED, token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn impersonate(
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
|
Authed { email, username, .. }: Authed,
|
||||||
|
Json(new_token): Json<NewToken>,
|
||||||
|
) -> Result<(StatusCode, String)> {
|
||||||
|
let token = rd_string(30);
|
||||||
|
let mut tx = db.begin().await?;
|
||||||
|
require_super_admin(&mut tx, &email).await?;
|
||||||
|
|
||||||
|
if new_token.impersonate_email.is_none() {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
"impersonate_username is required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let impersonated = new_token.impersonate_email.unwrap();
|
||||||
|
|
||||||
|
let is_super_admin = sqlx::query_scalar!(
|
||||||
|
"SELECT super_admin FROM password WHERE email = $1",
|
||||||
|
impersonated
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut tx)
|
||||||
|
.await?
|
||||||
|
.unwrap_or(false);
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO token
|
||||||
|
(token, email, label, expiration, super_admin)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
token,
|
||||||
|
impersonated,
|
||||||
|
new_token.label,
|
||||||
|
new_token.expiration,
|
||||||
|
is_super_admin
|
||||||
|
)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
audit_log(
|
||||||
|
&mut tx,
|
||||||
|
&username,
|
||||||
|
"users.impersonate",
|
||||||
|
ActionKind::Delete,
|
||||||
|
&"global",
|
||||||
|
Some(&token[0..10]),
|
||||||
|
Some([("impersonated", &format!("{impersonated}")[..])].into()),
|
||||||
|
)
|
||||||
|
.instrument(tracing::info_span!("token", email = &impersonated))
|
||||||
|
.await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok((StatusCode::CREATED, token))
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_tokens(
|
async fn list_tokens(
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Authed { email, .. }: Authed,
|
Authed { email, .. }: Authed,
|
||||||
@@ -1788,6 +1892,17 @@ pub async fn delete_expired_items_perdiodically(
|
|||||||
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
|
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pip_resolution_r = sqlx::query_scalar!(
|
||||||
|
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match pip_resolution_r {
|
||||||
|
Ok(res) => tracing::debug!("deleted {} pip_resolution: {:?}", res.len(), res),
|
||||||
|
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
|
||||||
|
}
|
||||||
|
|
||||||
let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
|
let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
|
||||||
"DELETE FROM magic_link WHERE expiration <= now()
|
"DELETE FROM magic_link WHERE expiration <= now()
|
||||||
RETURNING concat(substring(token for 10), '*****')",
|
RETURNING concat(substring(token for 10), '*****')",
|
||||||
|
|||||||
@@ -7,16 +7,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use sqlx::{Postgres, Transaction};
|
use sqlx::{Postgres, Transaction};
|
||||||
use windmill_common::error::{self, Error};
|
use windmill_common::{
|
||||||
|
error::{self, Error},
|
||||||
|
users::SUPERADMIN_SECRET_EMAIL,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn require_super_admin<'c>(
|
pub async fn require_super_admin<'c>(
|
||||||
db: &mut Transaction<'c, Postgres>,
|
db: &mut Transaction<'c, Postgres>,
|
||||||
email: &str,
|
email: &str,
|
||||||
) -> error::Result<()> {
|
) -> error::Result<()> {
|
||||||
|
if email == SUPERADMIN_SECRET_EMAIL {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||||
.fetch_one(db)
|
.fetch_optional(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !is_admin {
|
if !is_admin {
|
||||||
Err(Error::NotAuthorized(
|
Err(Error::NotAuthorized(
|
||||||
"This endpoint require caller to be a super admin".to_owned(),
|
"This endpoint require caller to be a super admin".to_owned(),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
oauth2::_refresh_token,
|
oauth2::_refresh_token,
|
||||||
users::{require_owner_of_path, Authed},
|
users::{maybe_refresh_folders, require_owner_of_path, Authed},
|
||||||
webhook_util::{WebhookMessage, WebhookShared},
|
webhook_util::{WebhookMessage, WebhookShared},
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
@@ -26,6 +26,7 @@ use axum::{
|
|||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
|
use serde_json::Value;
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{Error, JsonResult, Result},
|
error::{Error, JsonResult, Result},
|
||||||
@@ -205,12 +206,15 @@ async fn check_path_conflict<'c>(
|
|||||||
|
|
||||||
async fn create_variable(
|
async fn create_variable(
|
||||||
authed: Authed,
|
authed: Authed,
|
||||||
|
Extension(db): Extension<DB>,
|
||||||
Extension(user_db): Extension<UserDB>,
|
Extension(user_db): Extension<UserDB>,
|
||||||
Extension(webhook): Extension<WebhookShared>,
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
|
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
|
||||||
Json(variable): Json<CreateVariable>,
|
Json(variable): Json<CreateVariable>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
|
let authed = maybe_refresh_folders(&variable.path, &w_id, authed, &db).await;
|
||||||
|
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
check_path_conflict(&mut tx, &w_id, &variable.path).await?;
|
check_path_conflict(&mut tx, &w_id, &variable.path).await?;
|
||||||
@@ -329,6 +333,7 @@ async fn update_variable(
|
|||||||
use sql_builder::prelude::*;
|
use sql_builder::prelude::*;
|
||||||
|
|
||||||
let path = path.to_path();
|
let path = path.to_path();
|
||||||
|
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
|
||||||
|
|
||||||
let mut tx = user_db.begin(&authed).await?;
|
let mut tx = user_db.begin(&authed).await?;
|
||||||
|
|
||||||
@@ -378,9 +383,27 @@ async fn update_variable(
|
|||||||
if !authed.is_admin {
|
if !authed.is_admin {
|
||||||
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
|
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
|
||||||
}
|
}
|
||||||
|
let mut v = sqlx::query_scalar!(
|
||||||
|
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||||
|
path,
|
||||||
|
w_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut tx)
|
||||||
|
.await?
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
if let Some(old_v) = v {
|
||||||
|
v = Some(replace_path(
|
||||||
|
old_v,
|
||||||
|
&format!("$var:{path}"),
|
||||||
|
&format!("$var:{npath}"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
|
||||||
npath,
|
npath,
|
||||||
|
v,
|
||||||
path,
|
path,
|
||||||
w_id
|
w_id
|
||||||
)
|
)
|
||||||
@@ -419,6 +442,23 @@ async fn update_variable(
|
|||||||
Ok(format!("variable {} updated (npath: {:?})", path, npath))
|
Ok(format!("variable {} updated (npath: {:?})", path, npath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
|
||||||
|
match v {
|
||||||
|
Value::Object(v) => Value::Object(
|
||||||
|
v.into_iter()
|
||||||
|
.map(|(k, v)| (k, replace_path(v, path, npath)))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
Value::Array(arr) => Value::Array(
|
||||||
|
arr.into_iter()
|
||||||
|
.map(|v| replace_path(v, path, npath))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
Value::String(s) if s == path => Value::String(npath.to_owned()),
|
||||||
|
_ => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn build_crypt<'c>(
|
pub async fn build_crypt<'c>(
|
||||||
db: &mut Transaction<'c, Postgres>,
|
db: &mut Transaction<'c, Postgres>,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::{select, sync::mpsc, time::interval};
|
use tokio::{select, sync::mpsc, time::interval};
|
||||||
|
use windmill_common::METRICS_ENABLED;
|
||||||
|
|
||||||
use crate::db::DB;
|
use crate::db::DB;
|
||||||
|
|
||||||
@@ -12,6 +13,26 @@ lazy_static::lazy_static! {
|
|||||||
"Histogram of webhook requests made"
|
"Histogram of webhook requests made"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
pub static ref INSTANCE_EVENTS_WEBHOOK: Option<String> = std::env::var("INSTANCE_EVENTS_WEBHOOK").ok();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum WebhookPayload {
|
||||||
|
WorkspaceEvent(String, WebhookMessage),
|
||||||
|
InstanceEvent(InstanceEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum InstanceEvent {
|
||||||
|
UserSignupOAuth { email: String },
|
||||||
|
UserAdded { email: String },
|
||||||
|
// UserDeleted { email: String },
|
||||||
|
// UserDeletedWorkspace { workspace: String, email: String },
|
||||||
|
UserAddedWorkspace { workspace: String, email: String },
|
||||||
|
UserInvitedWorkspace { workspace: String, email: String },
|
||||||
|
UserJoinedWorkspace { workspace: String, email: String, username: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -45,12 +66,12 @@ pub enum WebhookMessage {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WebhookShared {
|
pub struct WebhookShared {
|
||||||
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
|
pub channel: mpsc::UnboundedSender<WebhookPayload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebhookShared {
|
impl WebhookShared {
|
||||||
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
|
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
|
let (tx, mut rx) = mpsc::unbounded_channel::<WebhookPayload>();
|
||||||
let _process = tokio::spawn(async move {
|
let _process = tokio::spawn(async move {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
// TODO: investigate pool timeouts and such if TCP load is high
|
// TODO: investigate pool timeouts and such if TCP load is high
|
||||||
@@ -65,7 +86,7 @@ impl WebhookShared {
|
|||||||
biased;
|
biased;
|
||||||
_ = shutdown_rx.recv() => break,
|
_ = shutdown_rx.recv() => break,
|
||||||
r = rx.recv() => match r {
|
r = rx.recv() => match r {
|
||||||
Some((workspace_id, message)) => {
|
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
|
||||||
let url_guard = match cache.get(&workspace_id).await {
|
let url_guard = match cache.get(&workspace_id).await {
|
||||||
Some(guard) => {
|
Some(guard) => {
|
||||||
guard
|
guard
|
||||||
@@ -89,12 +110,19 @@ impl WebhookShared {
|
|||||||
};
|
};
|
||||||
let webook_opt = url_guard.value();
|
let webook_opt = url_guard.value();
|
||||||
if let Some(url) = webook_opt {
|
if let Some(url) = webook_opt {
|
||||||
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
|
let timer = if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
|
||||||
let _ = client.post(url).json(&message).send().await;
|
let _ = client.post(url).json(&message).send().await;
|
||||||
timer.stop_and_record();
|
timer.map(|x| x.stop_and_record());
|
||||||
drop(url_guard);
|
drop(url_guard);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Some(WebhookPayload::InstanceEvent(event)) => {
|
||||||
|
if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
|
||||||
|
let r = client.post(INSTANCE_EVENTS_WEBHOOK.as_ref().unwrap()).json(&event).send().await;
|
||||||
|
if let Err(e) = r {
|
||||||
|
tracing::error!("Error sending instance event: {}", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
None => break,
|
None => break,
|
||||||
},
|
},
|
||||||
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
|
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
|
||||||
@@ -109,6 +137,16 @@ impl WebhookShared {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
|
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
|
||||||
let _ = self.channel.send((workspace_id.clone(), message));
|
let _ = self.channel.send(WebhookPayload::WorkspaceEvent(
|
||||||
|
workspace_id.clone(),
|
||||||
|
message,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_instance_event(&self, event: InstanceEvent) {
|
||||||
|
if INSTANCE_EVENTS_WEBHOOK.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = self.channel.send(WebhookPayload::InstanceEvent(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub fn global_service() -> Router {
|
|||||||
struct WorkerPing {
|
struct WorkerPing {
|
||||||
worker: String,
|
worker: String,
|
||||||
worker_instance: String,
|
worker_instance: String,
|
||||||
ping_at: chrono::DateTime<chrono::Utc>,
|
last_ping: Option<i32>,
|
||||||
started_at: chrono::DateTime<chrono::Utc>,
|
started_at: chrono::DateTime<chrono::Utc>,
|
||||||
ip: String,
|
ip: String,
|
||||||
jobs_executed: i32,
|
jobs_executed: i32,
|
||||||
@@ -45,7 +45,7 @@ async fn list_worker_pings(
|
|||||||
|
|
||||||
let rows = sqlx::query_as!(
|
let rows = sqlx::query_as!(
|
||||||
WorkerPing,
|
WorkerPing,
|
||||||
"SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
|
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
|
||||||
per_page as i64,
|
per_page as i64,
|
||||||
offset as i64
|
offset as i64
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ use crate::{
|
|||||||
db::{UserDB, DB},
|
db::{UserDB, DB},
|
||||||
folders::Folder,
|
folders::Folder,
|
||||||
resources::{Resource, ResourceType},
|
resources::{Resource, ResourceType},
|
||||||
users::{Authed, WorkspaceInvite, NEW_USER_WEBHOOK},
|
users::{Authed, WorkspaceInvite, VALID_USERNAME},
|
||||||
utils::require_super_admin,
|
utils::require_super_admin,
|
||||||
HTTP_CLIENT,
|
variables::build_crypt,
|
||||||
|
webhook_util::{InstanceEvent, WebhookShared},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
@@ -30,6 +31,7 @@ use axum::{
|
|||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use magic_crypt::MagicCryptTrait;
|
||||||
#[cfg(feature = "enterprise")]
|
#[cfg(feature = "enterprise")]
|
||||||
use stripe::CustomerId;
|
use stripe::CustomerId;
|
||||||
use windmill_audit::{audit_log, ActionKind};
|
use windmill_audit::{audit_log, ActionKind};
|
||||||
@@ -941,11 +943,14 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
|
|||||||
async fn invite_user(
|
async fn invite_user(
|
||||||
Authed { username, is_admin, .. }: Authed,
|
Authed { username, is_admin, .. }: Authed,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(nu): Json<NewWorkspaceInvite>,
|
Json(mut nu): Json<NewWorkspaceInvite>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
require_admin(is_admin, &username)?;
|
require_admin(is_admin, &username)?;
|
||||||
|
|
||||||
|
nu.email = nu.email.to_lowercase();
|
||||||
|
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -962,14 +967,10 @@ async fn invite_user(
|
|||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
webhook.send_instance_event(InstanceEvent::UserInvitedWorkspace {
|
||||||
let _ = &HTTP_CLIENT
|
email: nu.email.clone(),
|
||||||
.post(&new_user_webhook)
|
workspace: w_id,
|
||||||
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_invite"}))
|
});
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
@@ -980,12 +981,19 @@ async fn invite_user(
|
|||||||
async fn add_user(
|
async fn add_user(
|
||||||
Authed { username, is_admin, .. }: Authed,
|
Authed { username, is_admin, .. }: Authed,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
|
Extension(webhook): Extension<WebhookShared>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Json(nu): Json<NewWorkspaceUser>,
|
Json(mut nu): Json<NewWorkspaceUser>,
|
||||||
) -> Result<(StatusCode, String)> {
|
) -> Result<(StatusCode, String)> {
|
||||||
require_admin(is_admin, &username)?;
|
require_admin(is_admin, &username)?;
|
||||||
|
nu.email = nu.email.to_lowercase();
|
||||||
|
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
|
if !VALID_USERNAME.is_match(&nu.username) {
|
||||||
|
return Err(windmill_common::error::Error::BadRequest(format!(
|
||||||
|
"Usermame can only contain alphanumeric characters and underscores"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO usr
|
"INSERT INTO usr
|
||||||
@@ -1000,16 +1008,22 @@ async fn add_user(
|
|||||||
.execute(&mut tx)
|
.execute(&mut tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query_as!(
|
||||||
|
Group,
|
||||||
|
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||||
|
&w_id,
|
||||||
|
nu.username,
|
||||||
|
"all",
|
||||||
|
)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
webhook.send_instance_event(InstanceEvent::UserAddedWorkspace {
|
||||||
let _ = HTTP_CLIENT
|
workspace: w_id.clone(),
|
||||||
.post(&new_user_webhook)
|
email: nu.email.clone(),
|
||||||
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_add"}))
|
});
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
@@ -1073,6 +1087,7 @@ struct ScriptMetadata {
|
|||||||
schema: Option<Schema>,
|
schema: Option<Schema>,
|
||||||
is_template: bool,
|
is_template: bool,
|
||||||
lock: Vec<String>,
|
lock: Vec<String>,
|
||||||
|
kind: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ArchiveImpl {
|
enum ArchiveImpl {
|
||||||
@@ -1124,6 +1139,7 @@ impl ArchiveImpl {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ArchiveQueryParams {
|
struct ArchiveQueryParams {
|
||||||
archive_type: Option<String>,
|
archive_type: Option<String>,
|
||||||
|
plain_secret: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -1169,7 +1185,7 @@ async fn tarball_workspace(
|
|||||||
authed: Authed,
|
authed: Authed,
|
||||||
Extension(db): Extension<DB>,
|
Extension(db): Extension<DB>,
|
||||||
Path(w_id): Path<String>,
|
Path(w_id): Path<String>,
|
||||||
Query(ArchiveQueryParams { archive_type }): Query<ArchiveQueryParams>,
|
Query(ArchiveQueryParams { archive_type, plain_secret }): Query<ArchiveQueryParams>,
|
||||||
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
|
) -> Result<([(headers::HeaderName, String); 2], impl IntoResponse)> {
|
||||||
require_admin(authed.is_admin, &authed.username)?;
|
require_admin(authed.is_admin, &authed.username)?;
|
||||||
|
|
||||||
@@ -1235,6 +1251,7 @@ async fn tarball_workspace(
|
|||||||
description: script.description,
|
description: script.description,
|
||||||
schema: script.schema,
|
schema: script.schema,
|
||||||
is_template: script.is_template,
|
is_template: script.is_template,
|
||||||
|
kind: script.kind.to_string(),
|
||||||
lock,
|
lock,
|
||||||
};
|
};
|
||||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||||
@@ -1305,7 +1322,15 @@ async fn tarball_workspace(
|
|||||||
.fetch_all(&db)
|
.fetch_all(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for var in variables {
|
let mc = build_crypt(&mut db.begin().await?, &w_id).await?;
|
||||||
|
|
||||||
|
for mut var in variables {
|
||||||
|
if plain_secret.unwrap_or(false) && var.value.is_some() && var.is_secret {
|
||||||
|
var.value = Some(
|
||||||
|
mc.decrypt_base64_to_string(var.value.unwrap())
|
||||||
|
.map_err(|e| Error::InternalErr(e.to_string()))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
let var_str = &to_string_without_metadata(&var, false).unwrap();
|
let var_str = &to_string_without_metadata(&var, false).unwrap();
|
||||||
archive
|
archive
|
||||||
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))
|
.write_to_archive(&var_str, &format!("{}.variable.json", var.path))
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ pub struct AuditLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip_all)]
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
pub async fn audit_log<'c>(
|
pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||||
db: &mut Transaction<'c, Postgres>,
|
db: E,
|
||||||
username: &str,
|
username: &str,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
action_kind: ActionKind,
|
action_kind: ActionKind,
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{collections::HashMap, time::Duration};
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{self, Deserialize, Serialize};
|
use serde::{self, Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
more_serde::{
|
more_serde::{
|
||||||
@@ -149,9 +152,6 @@ pub struct Suspend {
|
|||||||
pub struct FlowModule {
|
pub struct FlowModule {
|
||||||
#[serde(default = "default_id")]
|
#[serde(default = "default_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[serde(default)]
|
|
||||||
#[serde(alias = "input_transform")]
|
|
||||||
pub input_transforms: HashMap<String, InputTransform>,
|
|
||||||
pub value: FlowModuleValue,
|
pub value: FlowModuleValue,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub stop_after_if: Option<StopAfterIf>,
|
pub stop_after_if: Option<StopAfterIf>,
|
||||||
@@ -245,7 +245,7 @@ pub enum FlowModuleValue {
|
|||||||
},
|
},
|
||||||
RawScript {
|
RawScript {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "input_transform")]
|
#[serde(alias = "input_transform", serialize_with = "ordered_map")]
|
||||||
input_transforms: HashMap<String, InputTransform>,
|
input_transforms: HashMap<String, InputTransform>,
|
||||||
content: String,
|
content: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -257,6 +257,14 @@ pub enum FlowModuleValue {
|
|||||||
Identity,
|
Identity,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ordered_map<S>(value: &HashMap<String, InputTransform>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let ordered: BTreeMap<_, _> = value.iter().collect();
|
||||||
|
ordered.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ListFlowQuery {
|
pub struct ListFlowQuery {
|
||||||
pub path_start: Option<String>,
|
pub path_start: Option<String>,
|
||||||
|
|||||||
1
backend/windmill-common/src/jobs.rs
Normal file
1
backend/windmill-common/src/jobs.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
|
|
||||||
@@ -30,7 +30,19 @@ pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
|
|||||||
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
|
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
|
pub static ref METRICS_ADDR: Option<SocketAddr> = std::env::var("METRICS_ADDR")
|
||||||
|
.ok()
|
||||||
|
.map(|s| {
|
||||||
|
s.parse::<bool>()
|
||||||
|
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
|
||||||
|
.or_else(|_| s.parse::<SocketAddr>().map(Some))
|
||||||
|
})
|
||||||
|
.transpose().ok()
|
||||||
|
.flatten()
|
||||||
|
.flatten();
|
||||||
|
pub static ref METRICS_ENABLED: bool = METRICS_ADDR.is_some();
|
||||||
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
|
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
|
||||||
|
pub static ref IS_READY: Arc<std::sync::atomic::AtomicBool> = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tokio")]
|
#[cfg(feature = "tokio")]
|
||||||
@@ -58,14 +70,31 @@ pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::
|
|||||||
pub async fn serve_metrics(
|
pub async fn serve_metrics(
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||||
|
ready_worker_endpoint: bool,
|
||||||
) -> Result<(), hyper::Error> {
|
) -> Result<(), hyper::Error> {
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
axum::Server::bind(&addr)
|
use hyper::StatusCode;
|
||||||
.serve(
|
let router = Router::new().route("/metrics", get(metrics));
|
||||||
Router::new()
|
|
||||||
.route("/metrics", get(metrics))
|
let router = if ready_worker_endpoint {
|
||||||
.into_make_service(),
|
router.route(
|
||||||
|
"/ready",
|
||||||
|
get(|| async {
|
||||||
|
if IS_READY.load(Ordering::Relaxed) {
|
||||||
|
(StatusCode::OK, "ready")
|
||||||
|
} else {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "not ready")
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
router
|
||||||
|
};
|
||||||
|
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(router.into_make_service())
|
||||||
.with_graceful_shutdown(async {
|
.with_graceful_shutdown(async {
|
||||||
rx.recv().await.ok();
|
rx.recv().await.ok();
|
||||||
println!("Graceful shutdown of metrics");
|
println!("Graceful shutdown of metrics");
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub struct Schedule {
|
|||||||
pub edited_by: String,
|
pub edited_by: String,
|
||||||
pub edited_at: DateTime<chrono::Utc>,
|
pub edited_at: DateTime<chrono::Utc>,
|
||||||
pub schedule: String,
|
pub schedule: String,
|
||||||
pub offset_: i32,
|
pub timezone: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub script_path: String,
|
pub script_path: String,
|
||||||
pub is_flow: bool,
|
pub is_flow: bool,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::{self, Display},
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ use serde_json::to_string_pretty;
|
|||||||
|
|
||||||
use crate::utils::StripPath;
|
use crate::utils::StripPath;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Hash, Eq)]
|
||||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
|
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "sqlx",
|
feature = "sqlx",
|
||||||
@@ -103,6 +103,18 @@ pub enum ScriptKind {
|
|||||||
Approval,
|
Approval,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for ScriptKind {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt.write_str(match self {
|
||||||
|
ScriptKind::Trigger => "trigger",
|
||||||
|
ScriptKind::Failure => "failure",
|
||||||
|
ScriptKind::Script => "script",
|
||||||
|
ScriptKind::Approval => "approval",
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||||
pub struct Script {
|
pub struct Script {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pub const SUPERADMIN_SECRET_EMAIL: &str = "superadmin_secret@windmill.dev";
|
||||||
|
|
||||||
pub fn username_to_permissioned_as(user: &str) -> String {
|
pub fn username_to_permissioned_as(user: &str) -> String {
|
||||||
if user.contains('@') {
|
if user.contains('@') {
|
||||||
user.to_string()
|
user.to_string()
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
pub const MAX_PER_PAGE: usize = 10000;
|
pub const MAX_PER_PAGE: usize = 10000;
|
||||||
pub const DEFAULT_PER_PAGE: usize = 1000;
|
pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||||
@@ -19,7 +19,8 @@ pub struct Pagination {
|
|||||||
pub page: Option<usize>,
|
pub page: Option<usize>,
|
||||||
pub per_page: Option<usize>,
|
pub per_page: Option<usize>,
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct StripPath(pub String);
|
pub struct StripPath(pub String);
|
||||||
|
|
||||||
impl StripPath {
|
impl StripPath {
|
||||||
@@ -51,8 +52,8 @@ pub fn paginate(pagination: Pagination) -> (usize, usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sqlx")]
|
#[cfg(feature = "sqlx")]
|
||||||
pub async fn now_from_db<'c>(
|
pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>(
|
||||||
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
|
db: E,
|
||||||
) -> Result<chrono::DateTime<chrono::Utc>> {
|
) -> Result<chrono::DateTime<chrono::Utc>> {
|
||||||
Ok(sqlx::query_scalar!("SELECT now()")
|
Ok(sqlx::query_scalar!("SELECT now()")
|
||||||
.fetch_one(db)
|
.fetch_one(db)
|
||||||
@@ -118,3 +119,9 @@ pub fn rd_string(len: usize) -> String {
|
|||||||
.map(char::from)
|
.map(char::from)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn calculate_hash(s: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(s);
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,8 +25,12 @@ serde_json.workspace = true
|
|||||||
ulid.workspace = true
|
ulid.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
chrono-tz.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
prometheus.workspace = true
|
prometheus.workspace = true
|
||||||
cron.workspace = true
|
cron.workspace = true
|
||||||
|
rsmq_async.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
futures-core.workspace = true
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ use windmill_common::{
|
|||||||
flows::{FlowModule, FlowModuleValue, FlowValue},
|
flows::{FlowModule, FlowModuleValue, FlowValue},
|
||||||
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
|
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
|
||||||
utils::StripPath,
|
utils::StripPath,
|
||||||
|
METRICS_ENABLED,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::QueueTransaction;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||||
.user_agent("windmill/beta")
|
.user_agent("windmill/beta")
|
||||||
@@ -51,32 +54,43 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
const MAX_FREE_EXECS: i32 = 1000;
|
const MAX_FREE_EXECS: i32 = 1000;
|
||||||
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
|
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
|
||||||
|
const RSMQ_MAIN_QUEUE: &'static str = "main_queue";
|
||||||
|
|
||||||
pub async fn cancel_job<'c>(
|
pub async fn cancel_job<'c, R: rsmq_async::RsmqConnection + Clone>(
|
||||||
username: &str,
|
username: &str,
|
||||||
reason: Option<String>,
|
reason: Option<String>,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
mut tx: Transaction<'c, Postgres>,
|
mut tx: Transaction<'c, Postgres>,
|
||||||
|
rsmq: Option<R>,
|
||||||
|
force_rerun: bool,
|
||||||
) -> error::Result<(Transaction<'c, Postgres>, Option<Uuid>)> {
|
) -> error::Result<(Transaction<'c, Postgres>, Option<Uuid>)> {
|
||||||
let job_option = sqlx::query_scalar!(
|
let job_option = sqlx::query_scalar!(
|
||||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0 WHERE id = $3 \
|
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, scheduled_for = now(), suspend = 0, running = CASE WHEN $3 THEN false ELSE running END WHERE id = $4 \
|
||||||
AND workspace_id = $4 RETURNING id",
|
AND workspace_id = $5 RETURNING id",
|
||||||
username,
|
username,
|
||||||
reason,
|
reason,
|
||||||
|
force_rerun,
|
||||||
id,
|
id,
|
||||||
w_id
|
w_id
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut tx)
|
.fetch_optional(&mut tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
if let Some(mut rsmq) = rsmq {
|
||||||
|
rsmq.change_message_visibility(RSMQ_MAIN_QUEUE, &id.to_string(), 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut jobs = job_option.map(|j| vec![j]).unwrap_or_default();
|
let mut jobs = job_option.map(|j| vec![j]).unwrap_or_default();
|
||||||
while !jobs.is_empty() {
|
while !jobs.is_empty() {
|
||||||
let p_job = jobs.pop();
|
let p_job = jobs.pop();
|
||||||
let new_jobs = sqlx::query_scalar!(
|
let new_jobs = sqlx::query_scalar!(
|
||||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE parent_job = $3 \
|
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2, running = CASE WHEN $3 THEN false ELSE running END WHERE parent_job = $4 \
|
||||||
AND workspace_id = $4 RETURNING id",
|
AND workspace_id = $5 RETURNING id",
|
||||||
username,
|
username,
|
||||||
reason,
|
reason,
|
||||||
|
force_rerun,
|
||||||
p_job,
|
p_job,
|
||||||
w_id
|
w_id
|
||||||
)
|
)
|
||||||
@@ -87,10 +101,11 @@ pub async fn cancel_job<'c>(
|
|||||||
Ok((tx, job_option))
|
Ok((tx, job_option))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pull(
|
pub async fn pull<R: rsmq_async::RsmqConnection + Clone>(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
whitelist_workspaces: Option<Vec<String>>,
|
whitelist_workspaces: Option<Vec<String>>,
|
||||||
blacklist_workspaces: Option<Vec<String>>,
|
blacklist_workspaces: Option<Vec<String>>,
|
||||||
|
rsmq: Option<R>,
|
||||||
) -> windmill_common::error::Result<Option<QueuedJob>> {
|
) -> windmill_common::error::Result<Option<QueuedJob>> {
|
||||||
let mut workspaces_filter = String::new();
|
let mut workspaces_filter = String::new();
|
||||||
if let Some(whitelist) = whitelist_workspaces {
|
if let Some(whitelist) = whitelist_workspaces {
|
||||||
@@ -102,6 +117,9 @@ pub async fn pull(
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(",")
|
.join(",")
|
||||||
));
|
));
|
||||||
|
if let Some(_rsmq) = rsmq {
|
||||||
|
todo!("REDIS: Implement workspace filters for redis");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(blacklist) = blacklist_workspaces {
|
if let Some(blacklist) = blacklist_workspaces {
|
||||||
workspaces_filter.push_str(&format!(
|
workspaces_filter.push_str(&format!(
|
||||||
@@ -112,16 +130,50 @@ pub async fn pull(
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(",")
|
.join(",")
|
||||||
));
|
));
|
||||||
|
if let Some(_rsmq) = rsmq {
|
||||||
|
todo!("REDIS: Implement workspace filters for redis");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Jobs can be started if they:
|
|
||||||
* - haven't been started before,
|
let job: Option<QueuedJob> = if let Some(mut rsmq) = rsmq {
|
||||||
* running = false
|
// TODO: REDIS: Race conditions / replace last_ping
|
||||||
* - are flows with a step that needed resume,
|
let msg = rsmq
|
||||||
* suspend_until is non-null
|
.pop_message::<Vec<u8>>(RSMQ_MAIN_QUEUE)
|
||||||
* and suspend = 0 when the resume messages are received
|
.await
|
||||||
* or suspend_until <= now() if it has timed out */
|
.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(&format!(
|
|
||||||
"UPDATE queue
|
if let Some(msg) = msg {
|
||||||
|
let uuid = Uuid::from_bytes_le(
|
||||||
|
msg.message
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| anyhow::anyhow!("Failed to parsed Redis message"))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
sqlx::query_as::<_, QueuedJob>(
|
||||||
|
"UPDATE queue
|
||||||
|
SET running = true
|
||||||
|
, started_at = coalesce(started_at, now())
|
||||||
|
, last_ping = now()
|
||||||
|
, suspend_until = null
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *",
|
||||||
|
)
|
||||||
|
.bind(uuid)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Jobs can be started if they:
|
||||||
|
* - haven't been started before,
|
||||||
|
* running = false
|
||||||
|
* - are flows with a step that needed resume,
|
||||||
|
* suspend_until is non-null
|
||||||
|
* and suspend = 0 when the resume messages are received
|
||||||
|
* or suspend_until <= now() if it has timed out */
|
||||||
|
sqlx::query_as::<_, QueuedJob>(&format!(
|
||||||
|
"UPDATE queue
|
||||||
SET running = true
|
SET running = true
|
||||||
, started_at = coalesce(started_at, now())
|
, started_at = coalesce(started_at, now())
|
||||||
, last_ping = now()
|
, last_ping = now()
|
||||||
@@ -139,11 +191,12 @@ pub async fn pull(
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
RETURNING *"
|
RETURNING *"
|
||||||
))
|
))
|
||||||
.fetch_optional(db)
|
.fetch_optional(db)
|
||||||
.await?;
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
if job.is_some() {
|
if job.is_some() && *METRICS_ENABLED {
|
||||||
QUEUE_PULL_COUNT.inc();
|
QUEUE_PULL_COUNT.inc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,24 +256,26 @@ pub async fn get_result_by_id(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn delete_job(
|
pub async fn delete_job<'c, R: rsmq_async::RsmqConnection + Clone + Send>(
|
||||||
db: &Pool<Postgres>,
|
mut tx: QueueTransaction<'c, R>,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
job_id: Uuid,
|
job_id: Uuid,
|
||||||
) -> windmill_common::error::Result<()> {
|
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
|
||||||
QUEUE_DELETE_COUNT.inc();
|
if *METRICS_ENABLED {
|
||||||
|
QUEUE_DELETE_COUNT.inc();
|
||||||
|
}
|
||||||
let job_removed = sqlx::query_scalar!(
|
let job_removed = sqlx::query_scalar!(
|
||||||
"DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1",
|
"DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1",
|
||||||
w_id,
|
w_id,
|
||||||
job_id
|
job_id
|
||||||
)
|
)
|
||||||
.fetch_one(db)
|
.fetch_one(&mut tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("Error during deletion of job {job_id}: {e}")))?
|
.map_err(|e| Error::InternalErr(format!("Error during deletion of job {job_id}: {e}")))?
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
== 1;
|
== 1;
|
||||||
tracing::debug!("Job {job_id} deleted: {job_removed}");
|
tracing::debug!("Job {job_id} deleted: {job_removed}");
|
||||||
Ok(())
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_queued_job<'c>(
|
pub async fn get_queued_job<'c>(
|
||||||
@@ -239,9 +294,9 @@ pub async fn get_queued_job<'c>(
|
|||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
// #[instrument(level = "trace", skip_all)]
|
||||||
pub async fn push<'c>(
|
pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||||
mut tx: Transaction<'c, Postgres>,
|
mut tx: QueueTransaction<'c, R>,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
job_payload: JobPayload,
|
job_payload: JobPayload,
|
||||||
args: serde_json::Map<String, serde_json::Value>,
|
args: serde_json::Map<String, serde_json::Value>,
|
||||||
@@ -256,14 +311,13 @@ pub async fn push<'c>(
|
|||||||
mut same_worker: bool,
|
mut same_worker: bool,
|
||||||
pre_run_error: Option<&windmill_common::error::Error>,
|
pre_run_error: Option<&windmill_common::error::Error>,
|
||||||
visible_to_owner: bool,
|
visible_to_owner: bool,
|
||||||
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
|
) -> Result<(Uuid, QueueTransaction<'c, R>), Error> {
|
||||||
let scheduled_for = scheduled_for_o.unwrap_or_else(chrono::Utc::now);
|
|
||||||
let args_json = serde_json::Value::Object(args);
|
let args_json = serde_json::Value::Object(args);
|
||||||
let job_id: Uuid = Ulid::new().into();
|
let job_id: Uuid = Ulid::new().into();
|
||||||
|
|
||||||
if cfg!(feature = "enterprise") {
|
if cfg!(feature = "enterprise") {
|
||||||
let premium_workspace =
|
let premium_workspace = *CLOUD_HOSTED
|
||||||
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
|
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id)
|
||||||
.fetch_one(&mut tx)
|
.fetch_one(&mut tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -486,7 +540,6 @@ pub async fn push<'c>(
|
|||||||
modules.push(FlowModule {
|
modules.push(FlowModule {
|
||||||
id: format!("{}-v", flow.modules[flow.modules.len() - 1].id),
|
id: format!("{}-v", flow.modules[flow.modules.len() - 1].id),
|
||||||
value: FlowModuleValue::Identity,
|
value: FlowModuleValue::Identity,
|
||||||
input_transforms: HashMap::new(),
|
|
||||||
stop_after_if: None,
|
stop_after_if: None,
|
||||||
summary: Some(
|
summary: Some(
|
||||||
"Virtual module needed for suspend/sleep when last module".to_string(),
|
"Virtual module needed for suspend/sleep when last module".to_string(),
|
||||||
@@ -510,7 +563,7 @@ pub async fn push<'c>(
|
|||||||
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
|
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
|
||||||
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
|
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
|
||||||
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
|
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
|
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, now()), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
|
||||||
RETURNING id",
|
RETURNING id",
|
||||||
workspace_id,
|
workspace_id,
|
||||||
job_id,
|
job_id,
|
||||||
@@ -518,7 +571,7 @@ pub async fn push<'c>(
|
|||||||
parent_job,
|
parent_job,
|
||||||
user,
|
user,
|
||||||
permissioned_as,
|
permissioned_as,
|
||||||
scheduled_for,
|
scheduled_for_o,
|
||||||
script_hash,
|
script_hash,
|
||||||
script_path.clone(),
|
script_path.clone(),
|
||||||
raw_code,
|
raw_code,
|
||||||
@@ -540,7 +593,9 @@ pub async fn push<'c>(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
|
||||||
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
|
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
|
||||||
QUEUE_PUSH_COUNT.inc();
|
if *METRICS_ENABLED {
|
||||||
|
QUEUE_PUSH_COUNT.inc();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let uuid_string = job_id.to_string();
|
let uuid_string = job_id.to_string();
|
||||||
@@ -575,6 +630,10 @@ pub async fn push<'c>(
|
|||||||
.instrument(tracing::info_span!("job_run", email = &email))
|
.instrument(tracing::info_span!("job_run", email = &email))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
if let Some(ref mut rsmq) = tx.rsmq {
|
||||||
|
rsmq.send_message(job_id.to_bytes_le().to_vec(), scheduled_for_o);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((uuid, tx))
|
Ok((uuid, tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +717,7 @@ impl QueuedJob {
|
|||||||
self.script_path
|
self.script_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
.unwrap_or("NO_FLOW_PATH")
|
.unwrap_or("tmp/main")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
mod jobs;
|
mod jobs;
|
||||||
|
mod queue_transaction;
|
||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
|
|
||||||
pub use jobs::*;
|
pub use jobs::*;
|
||||||
|
pub use queue_transaction::*;
|
||||||
|
|||||||
161
backend/windmill-queue/src/queue_transaction.rs
Normal file
161
backend/windmill-queue/src/queue_transaction.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use futures_core::{future::BoxFuture, stream::BoxStream};
|
||||||
|
use rsmq_async::{RedisBytes, RsmqConnection};
|
||||||
|
use sqlx::{Postgres, Transaction};
|
||||||
|
|
||||||
|
pub enum RedisOp {
|
||||||
|
SendMessage(RedisBytes, Option<chrono::DateTime<chrono::Utc>>),
|
||||||
|
DeleteMessage(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisOp {
|
||||||
|
pub async fn apply<R: RsmqConnection>(self, rsmq: &mut R) -> Result<(), rsmq_async::RsmqError> {
|
||||||
|
match self {
|
||||||
|
RedisOp::SendMessage(bytes, time) => {
|
||||||
|
rsmq.send_message(
|
||||||
|
"main_queue",
|
||||||
|
bytes,
|
||||||
|
time.map(|t| (t - chrono::Utc::now()).num_seconds())
|
||||||
|
.and_then(|e| e.try_into().ok()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
RedisOp::DeleteMessage(id) => {
|
||||||
|
rsmq.delete_message("main_queue", &id).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RedisTransaction<R: RsmqConnection> {
|
||||||
|
rsmq: R,
|
||||||
|
queued_ops: Vec<RedisOp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: RsmqConnection> From<R> for RedisTransaction<R> {
|
||||||
|
fn from(value: R) -> Self {
|
||||||
|
Self { rsmq: value, queued_ops: Vec::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: RsmqConnection> RedisTransaction<R> {
|
||||||
|
pub async fn commit(self) -> Result<(), rsmq_async::RsmqError> {
|
||||||
|
let mut rsmq = self.rsmq;
|
||||||
|
for op in self.queued_ops {
|
||||||
|
op.apply(&mut rsmq).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_message<E: Into<RedisBytes>>(
|
||||||
|
&mut self,
|
||||||
|
bytes: E,
|
||||||
|
delay_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) {
|
||||||
|
self.queued_ops
|
||||||
|
.push(RedisOp::SendMessage(bytes.into(), delay_until))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_message(&mut self, id: String) {
|
||||||
|
self.queued_ops.push(RedisOp::DeleteMessage(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QueueTransaction<'c, R: RsmqConnection> {
|
||||||
|
pub rsmq: Option<RedisTransaction<R>>,
|
||||||
|
transaction: Transaction<'c, Postgres>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'c, R: RsmqConnection> From<(Option<R>, Transaction<'c, Postgres>)>
|
||||||
|
for QueueTransaction<'c, R>
|
||||||
|
{
|
||||||
|
fn from(value: (Option<R>, Transaction<'c, Postgres>)) -> Self {
|
||||||
|
Self { rsmq: value.0.map(|e| e.into()), transaction: value.1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'c, R: RsmqConnection> Debug for QueueTransaction<'c, R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("QueueTransaction")
|
||||||
|
.field("rsmq", &self.rsmq.as_ref().map(|_| ())) // do not require R: Debug
|
||||||
|
.field("transaction", &self.transaction)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'c, R: RsmqConnection> QueueTransaction<'c, R> {
|
||||||
|
pub async fn commit(self) -> Result<(), windmill_common::error::Error> {
|
||||||
|
self.transaction.commit().await?;
|
||||||
|
if let Some(rsmq) = self.rsmq {
|
||||||
|
rsmq.commit().await.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transaction_mut<'a>(&'a mut self) -> &'a mut Transaction<'c, Postgres> {
|
||||||
|
&mut self.transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'c, 'b, R: RsmqConnection + Send> sqlx::Executor<'b> for &'b mut QueueTransaction<'c, R> {
|
||||||
|
type Database = Postgres;
|
||||||
|
|
||||||
|
fn fetch_many<'e, 'q: 'e, E: 'q>(
|
||||||
|
self,
|
||||||
|
query: E,
|
||||||
|
) -> BoxStream<
|
||||||
|
'e,
|
||||||
|
Result<
|
||||||
|
sqlx::Either<
|
||||||
|
<Self::Database as sqlx::Database>::QueryResult,
|
||||||
|
<Self::Database as sqlx::Database>::Row,
|
||||||
|
>,
|
||||||
|
sqlx::Error,
|
||||||
|
>,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
'b: 'e,
|
||||||
|
E: sqlx::Execute<'q, Self::Database>,
|
||||||
|
{
|
||||||
|
self.transaction.fetch_many(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_optional<'e, 'q: 'e, E: 'q>(
|
||||||
|
self,
|
||||||
|
query: E,
|
||||||
|
) -> BoxFuture<'e, Result<Option<<Self::Database as sqlx::Database>::Row>, sqlx::Error>>
|
||||||
|
where
|
||||||
|
'b: 'e,
|
||||||
|
E: sqlx::Execute<'q, Self::Database>,
|
||||||
|
{
|
||||||
|
self.transaction.fetch_optional(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_with<'e, 'q: 'e>(
|
||||||
|
self,
|
||||||
|
sql: &'q str,
|
||||||
|
parameters: &'e [<Self::Database as sqlx::Database>::TypeInfo],
|
||||||
|
) -> BoxFuture<
|
||||||
|
'e,
|
||||||
|
Result<<Self::Database as sqlx::database::HasStatement<'q>>::Statement, sqlx::Error>,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
'b: 'e,
|
||||||
|
{
|
||||||
|
self.transaction.prepare_with(sql, parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe<'e, 'q: 'e>(
|
||||||
|
self,
|
||||||
|
sql: &'q str,
|
||||||
|
) -> BoxFuture<'e, Result<sqlx::Describe<Self::Database>, sqlx::Error>>
|
||||||
|
where
|
||||||
|
'b: 'e,
|
||||||
|
{
|
||||||
|
self.transaction.describe(sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,33 +6,39 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::str::FromStr;
|
use crate::{push, JobPayload};
|
||||||
|
|
||||||
use chrono::Duration;
|
|
||||||
use sqlx::{query_scalar, Postgres, Transaction};
|
use sqlx::{query_scalar, Postgres, Transaction};
|
||||||
|
use std::str::FromStr;
|
||||||
use windmill_common::{
|
use windmill_common::{
|
||||||
error::{self, Result},
|
error::{self, Result},
|
||||||
schedule::Schedule,
|
schedule::Schedule,
|
||||||
users::username_to_permissioned_as,
|
users::username_to_permissioned_as,
|
||||||
utils::{now_from_db, StripPath},
|
utils::{now_from_db, StripPath},
|
||||||
};
|
};
|
||||||
|
use crate::{QueueTransaction};
|
||||||
|
|
||||||
use crate::{push, JobPayload};
|
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||||
|
mut tx: QueueTransaction<'c, R>,
|
||||||
pub async fn push_scheduled_job<'c>(
|
|
||||||
mut tx: Transaction<'c, Postgres>,
|
|
||||||
schedule: Schedule,
|
schedule: Schedule,
|
||||||
) -> Result<Transaction<'c, Postgres>> {
|
) -> Result<QueueTransaction<'c, R>> {
|
||||||
let sched = cron::Schedule::from_str(&schedule.schedule)
|
let sched = cron::Schedule::from_str(&schedule.schedule)
|
||||||
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||||
|
|
||||||
let offset = Duration::minutes(schedule.offset_.into());
|
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
|
||||||
let now = now_from_db(&mut tx).await?;
|
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
|
||||||
|
|
||||||
|
let now = now_from_db(&mut tx).await?.with_timezone(&tz);
|
||||||
|
|
||||||
let next = sched
|
let next = sched
|
||||||
.after(&(now - offset + Duration::seconds(1)))
|
.after(&now)
|
||||||
.next()
|
.next()
|
||||||
.expect("a schedule should have a next event")
|
.expect("a schedule should have a next event");
|
||||||
+ offset;
|
|
||||||
|
// println!("next event ({:?}): {}", tz, next);
|
||||||
|
// println!("next event(UTC): {}", next.with_timezone(&chrono::Utc));
|
||||||
|
|
||||||
|
// Scheduled events must be stored in the database in UTC
|
||||||
|
let next = next.with_timezone(&chrono::Utc);
|
||||||
|
|
||||||
let already_exists: bool = query_scalar!(
|
let already_exists: bool = query_scalar!(
|
||||||
"SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
|
"SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",
|
||||||
@@ -65,7 +71,7 @@ pub async fn push_scheduled_job<'c>(
|
|||||||
} else {
|
} else {
|
||||||
JobPayload::ScriptHash {
|
JobPayload::ScriptHash {
|
||||||
hash: windmill_common::get_latest_hash_for_path(
|
hash: windmill_common::get_latest_hash_for_path(
|
||||||
&mut tx,
|
tx.transaction_mut(),
|
||||||
&schedule.workspace_id,
|
&schedule.workspace_id,
|
||||||
&schedule.script_path,
|
&schedule.script_path,
|
||||||
)
|
)
|
||||||
@@ -74,7 +80,15 @@ pub async fn push_scheduled_job<'c>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, mut tx) = push(
|
sqlx::query!(
|
||||||
|
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
|
||||||
|
&schedule.workspace_id,
|
||||||
|
&schedule.path
|
||||||
|
)
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (_, tx) = push(
|
||||||
tx,
|
tx,
|
||||||
&schedule.workspace_id,
|
&schedule.workspace_id,
|
||||||
payload,
|
payload,
|
||||||
@@ -92,14 +106,7 @@ pub async fn push_scheduled_job<'c>(
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
sqlx::query!(
|
Ok(tx) // TODO: Bubble up pushed UUID from here
|
||||||
"UPDATE schedule SET error = NULL WHERE workspace_id = $1 AND path = $2",
|
|
||||||
&schedule.workspace_id,
|
|
||||||
&schedule.path
|
|
||||||
)
|
|
||||||
.execute(&mut tx)
|
|
||||||
.await?;
|
|
||||||
Ok(tx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_schedule_opt<'c>(
|
pub async fn get_schedule_opt<'c>(
|
||||||
|
|||||||
@@ -48,3 +48,5 @@ deno_core.workspace = true
|
|||||||
const_format.workspace = true
|
const_format.workspace = true
|
||||||
git-version.workspace = true
|
git-version.workspace = true
|
||||||
dyn-iter.workspace = true
|
dyn-iter.workspace = true
|
||||||
|
once_cell.workspace = true
|
||||||
|
rsmq_async.workspace = true
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
name: "deno run script"
|
|
||||||
|
|
||||||
mode: ONCE
|
|
||||||
hostname: "deno"
|
|
||||||
log_level: ERROR
|
|
||||||
|
|
||||||
rlimit_as: 16000
|
|
||||||
rlimit_cpu: 1000
|
|
||||||
rlimit_fsize: 1000
|
|
||||||
rlimit_nofile: 10000
|
|
||||||
|
|
||||||
cwd: "/tmp"
|
|
||||||
|
|
||||||
|
|
||||||
clone_newnet: false
|
|
||||||
clone_newuser: {CLONE_NEWUSER}
|
|
||||||
|
|
||||||
keep_caps: false
|
|
||||||
keep_env: true
|
|
||||||
mount_proc: true
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/bin"
|
|
||||||
dst: "/bin"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/lib"
|
|
||||||
dst: "/lib"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/lib64"
|
|
||||||
dst: "/lib64"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/usr"
|
|
||||||
dst: "/usr"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/dev/null"
|
|
||||||
dst: "/dev/null"
|
|
||||||
is_bind: true
|
|
||||||
rw: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
dst: "/tmp"
|
|
||||||
fstype: "tmpfs"
|
|
||||||
rw: true
|
|
||||||
options: "size=500000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{JOB_DIR}/inner.ts"
|
|
||||||
dst: "/tmp/inner.ts"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{JOB_DIR}/main.ts"
|
|
||||||
dst: "/tmp/main.ts"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{JOB_DIR}/import_map.json"
|
|
||||||
dst: "/tmp/import_map.json"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{JOB_DIR}/lock.json"
|
|
||||||
dst: "/tmp/lock.json"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{JOB_DIR}/args.json"
|
|
||||||
dst: "/tmp/args.json"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{JOB_DIR}/result.json"
|
|
||||||
dst: "/tmp/result.json"
|
|
||||||
is_bind: true
|
|
||||||
rw: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/etc"
|
|
||||||
dst: "/etc"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/dev/random"
|
|
||||||
dst: "/dev/random"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "/dev/urandom"
|
|
||||||
dst: "/dev/urandom"
|
|
||||||
is_bind: true
|
|
||||||
}
|
|
||||||
|
|
||||||
mount {
|
|
||||||
src: "{CACHE_DIR}"
|
|
||||||
dst: "/tmp/.cache/deno"
|
|
||||||
is_bind: true
|
|
||||||
rw: true
|
|
||||||
mandatory: false
|
|
||||||
}
|
|
||||||
|
|
||||||
{SHARED_MOUNT}
|
|
||||||
|
|
||||||
iface_no_lo: true
|
|
||||||
|
|
||||||
envar: "DENO_DIR=/tmp/.cache/deno"
|
|
||||||
envar: "NO_COLOR=true"
|
|
||||||
envar: "HOME=/tmp"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ mount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount {
|
mount {
|
||||||
src: "{JOB_DIR}/inner.py"
|
src: "{JOB_DIR}/{MAIN}.py"
|
||||||
dst: "/tmp/inner.py"
|
dst: "/tmp/{MAIN}.py"
|
||||||
is_bind: true
|
is_bind: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ mount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount {
|
mount {
|
||||||
src: "{JOB_DIR}/main.py"
|
src: "{JOB_DIR}/wrapper.py"
|
||||||
dst: "/tmp/main.py"
|
dst: "/tmp/wrapper.py"
|
||||||
is_bind: true
|
is_bind: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,23 +6,30 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use sqlx::{Pool, Postgres, Transaction};
|
use sqlx::{Pool, Postgres};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Schedule};
|
use windmill_common::{
|
||||||
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob};
|
error::Error, flow_status::FlowStatusModule, schedule::Schedule, METRICS_ENABLED,
|
||||||
|
};
|
||||||
|
use windmill_queue::{
|
||||||
|
delete_job, schedule::get_schedule_opt, JobKind, QueueTransaction, QueuedJob, CLOUD_HOSTED,
|
||||||
|
};
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn add_completed_job_error(
|
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
queued_job: &QueuedJob,
|
queued_job: &QueuedJob,
|
||||||
logs: String,
|
logs: String,
|
||||||
e: serde_json::Value,
|
e: serde_json::Value,
|
||||||
metrics: Option<crate::worker::Metrics>,
|
metrics: Option<crate::worker::Metrics>,
|
||||||
|
rsmq: Option<R>,
|
||||||
) -> Result<serde_json::Value, Error> {
|
) -> Result<serde_json::Value, Error> {
|
||||||
metrics.map(|m| m.worker_execution_failed.inc());
|
if *METRICS_ENABLED {
|
||||||
|
metrics.map(|m| m.worker_execution_failed.inc());
|
||||||
|
}
|
||||||
let result = serde_json::json!({ "error": e });
|
let result = serde_json::json!({ "error": e });
|
||||||
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs).await?;
|
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs, rsmq).await?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +52,14 @@ fn flatten_jobs(modules: Vec<FlowStatusModule>) -> Vec<Uuid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn add_completed_job(
|
pub async fn add_completed_job<R: rsmq_async::RsmqConnection + Clone + Send>(
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
queued_job: &QueuedJob,
|
queued_job: &QueuedJob,
|
||||||
success: bool,
|
success: bool,
|
||||||
skipped: bool,
|
skipped: bool,
|
||||||
result: serde_json::Value,
|
result: serde_json::Value,
|
||||||
logs: String,
|
logs: String,
|
||||||
|
rsmq: Option<R>,
|
||||||
) -> Result<Uuid, Error> {
|
) -> Result<Uuid, Error> {
|
||||||
let duration =
|
let duration =
|
||||||
if queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview {
|
if queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview {
|
||||||
@@ -83,7 +91,7 @@ pub async fn add_completed_job(
|
|||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.flatten();
|
.flatten();
|
||||||
let mut tx = db.begin().await?;
|
let mut tx: QueueTransaction<'_, R> = (rsmq, db.begin().await?).into();
|
||||||
let job_id = queued_job.id.clone();
|
let job_id = queued_job.id.clone();
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO completed_job AS cj
|
"INSERT INTO completed_job AS cj
|
||||||
@@ -153,7 +161,7 @@ pub async fn add_completed_job(
|
|||||||
.execute(&mut tx)
|
.execute(&mut tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
|
||||||
let _ = delete_job(db, &queued_job.workspace_id, job_id).await?;
|
tx = delete_job(tx, &queued_job.workspace_id, job_id).await?;
|
||||||
if !queued_job.is_flow_step
|
if !queued_job.is_flow_step
|
||||||
&& queued_job.job_kind != JobKind::Flow
|
&& queued_job.job_kind != JobKind::Flow
|
||||||
&& queued_job.job_kind != JobKind::FlowPreview
|
&& queued_job.job_kind != JobKind::FlowPreview
|
||||||
@@ -175,8 +183,8 @@ pub async fn add_completed_job(
|
|||||||
let additional_usage = duration.unwrap() as i32 / 1000;
|
let additional_usage = duration.unwrap() as i32 / 1000;
|
||||||
|
|
||||||
let w_id = &queued_job.workspace_id;
|
let w_id = &queued_job.workspace_id;
|
||||||
let premium_workspace =
|
let premium_workspace = *CLOUD_HOSTED
|
||||||
sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
|
&& sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id)
|
||||||
.fetch_one(db)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::InternalErr(format!("fetching if {w_id} is premium: {e}")))?;
|
.map_err(|e| Error::InternalErr(format!("fetching if {w_id} is premium: {e}")))?;
|
||||||
@@ -208,21 +216,24 @@ pub async fn add_completed_job(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "trace", skip_all)]
|
#[instrument(level = "trace", skip_all)]
|
||||||
pub async fn schedule_again_if_scheduled<'c>(
|
pub async fn schedule_again_if_scheduled<'c, R: rsmq_async::RsmqConnection + Clone + Send + 'c>(
|
||||||
mut tx: Transaction<'c, Postgres>,
|
mut tx: QueueTransaction<'c, R>,
|
||||||
db: &Pool<Postgres>,
|
db: &Pool<Postgres>,
|
||||||
schedule_path: &str,
|
schedule_path: &str,
|
||||||
script_path: &str,
|
script_path: &str,
|
||||||
w_id: &str,
|
w_id: &str,
|
||||||
) -> windmill_common::error::Result<Transaction<'c, Postgres>> {
|
) -> windmill_common::error::Result<QueueTransaction<'c, R>> {
|
||||||
let schedule = get_schedule_opt(&mut tx, w_id, schedule_path)
|
let schedule = get_schedule_opt(tx.transaction_mut(), w_id, schedule_path).await?;
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
if schedule.is_none() {
|
||||||
Error::InternalErr(format!(
|
tracing::error!(
|
||||||
"Could not find schedule {:?} for workspace {}",
|
"Schedule {schedule_path} in {w_id} not found. Impossible to schedule again"
|
||||||
schedule_path, w_id
|
);
|
||||||
))
|
return Ok(tx);
|
||||||
})?;
|
}
|
||||||
|
|
||||||
|
let schedule = schedule.unwrap();
|
||||||
|
|
||||||
if schedule.enabled && script_path == schedule.script_path {
|
if schedule.enabled && script_path == schedule.script_path {
|
||||||
let res = windmill_queue::schedule::push_scheduled_job(
|
let res = windmill_queue::schedule::push_scheduled_job(
|
||||||
tx,
|
tx,
|
||||||
@@ -232,7 +243,7 @@ pub async fn schedule_again_if_scheduled<'c>(
|
|||||||
edited_by: schedule.edited_by,
|
edited_by: schedule.edited_by,
|
||||||
edited_at: schedule.edited_at,
|
edited_at: schedule.edited_at,
|
||||||
schedule: schedule.schedule,
|
schedule: schedule.schedule,
|
||||||
offset_: schedule.offset_,
|
timezone: schedule.timezone,
|
||||||
enabled: schedule.enabled,
|
enabled: schedule.enabled,
|
||||||
script_path: schedule.script_path,
|
script_path: schedule.script_path,
|
||||||
is_flow: schedule.is_flow,
|
is_flow: schedule.is_flow,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
* LICENSE-AGPL for a copy of the license.
|
* LICENSE-AGPL for a copy of the license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use deno_core::{op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, RuntimeOptions};
|
use deno_core::{
|
||||||
|
op, serde_v8, v8, v8::IsolateHandle, Extension, JsRuntime, OpState, RuntimeOptions,
|
||||||
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -17,10 +19,7 @@ use tokio::{sync::oneshot, time::timeout};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use windmill_common::{error::Error, flow_status::JobResult};
|
use windmill_common::{error::Error, flow_status::JobResult};
|
||||||
|
|
||||||
pub struct EvalCreds {
|
use crate::AuthedClient;
|
||||||
pub workspace: String,
|
|
||||||
pub token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IdContext {
|
pub struct IdContext {
|
||||||
@@ -29,22 +28,23 @@ pub struct IdContext {
|
|||||||
pub previous_id: String,
|
pub previous_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct OptAuthedClient(Option<AuthedClient>);
|
||||||
pub async fn eval_timeout(
|
pub async fn eval_timeout(
|
||||||
expr: String,
|
expr: String,
|
||||||
env: Vec<(String, serde_json::Value)>,
|
env: Vec<(String, serde_json::Value)>,
|
||||||
creds: Option<EvalCreds>,
|
authed_client: Option<&AuthedClient>,
|
||||||
by_id: Option<IdContext>,
|
by_id: Option<IdContext>,
|
||||||
base_internal_url: &str,
|
|
||||||
) -> anyhow::Result<serde_json::Value> {
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
let expr2 = expr.clone();
|
let expr2 = expr.clone();
|
||||||
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
|
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
|
||||||
let base_internal_url: String = base_internal_url.to_string();
|
let has_client = authed_client.is_some();
|
||||||
|
let authed_client = authed_client.cloned();
|
||||||
timeout(
|
timeout(
|
||||||
std::time::Duration::from_millis(3000),
|
std::time::Duration::from_millis(10000),
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut ops = vec![];
|
let mut ops = vec![];
|
||||||
|
|
||||||
if creds.is_some() {
|
if authed_client.is_some() {
|
||||||
ops.extend([
|
ops.extend([
|
||||||
// An op for summing an array of numbers
|
// An op for summing an array of numbers
|
||||||
// The op-layer automatically deserializes inputs
|
// The op-layer automatically deserializes inputs
|
||||||
@@ -54,12 +54,12 @@ pub async fn eval_timeout(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if by_id.is_some() {
|
if by_id.is_some() && authed_client.is_some() {
|
||||||
ops.push(op_get_result::decl());
|
ops.push(op_get_result::decl());
|
||||||
ops.push(op_get_id::decl());
|
ops.push(op_get_id::decl());
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext = Extension::builder().ops(ops).build();
|
let ext = Extension::builder("js_eval").ops(ops).build();
|
||||||
// Use our snapshot to provision our new runtime
|
// Use our snapshot to provision our new runtime
|
||||||
let options = RuntimeOptions {
|
let options = RuntimeOptions {
|
||||||
extensions: vec![ext],
|
extensions: vec![ext],
|
||||||
@@ -68,6 +68,11 @@ pub async fn eval_timeout(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut js_runtime = JsRuntime::new(options);
|
let mut js_runtime = JsRuntime::new(options);
|
||||||
|
{
|
||||||
|
let op_state = js_runtime.op_state();
|
||||||
|
let mut op_state = op_state.borrow_mut();
|
||||||
|
op_state.put(OptAuthedClient(authed_client.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(js_runtime.v8_isolate().thread_safe_handle())
|
.send(js_runtime.v8_isolate().thread_safe_handle())
|
||||||
@@ -86,14 +91,7 @@ pub async fn eval_timeout(
|
|||||||
|
|
||||||
let expr = replace_with_await_result(expr);
|
let expr = replace_with_await_result(expr);
|
||||||
|
|
||||||
let r = runtime.block_on(eval(
|
let r = runtime.block_on(eval(&mut js_runtime, &expr, env, by_id, has_client))?;
|
||||||
&mut js_runtime,
|
|
||||||
&expr,
|
|
||||||
env,
|
|
||||||
creds,
|
|
||||||
by_id,
|
|
||||||
&base_internal_url,
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(r) as anyhow::Result<Value>
|
Ok(r) as anyhow::Result<Value>
|
||||||
}),
|
}),
|
||||||
@@ -104,7 +102,7 @@ pub async fn eval_timeout(
|
|||||||
isolate.terminate_execution();
|
isolate.terminate_execution();
|
||||||
};
|
};
|
||||||
Error::ExecutionErr(format!(
|
Error::ExecutionErr(format!(
|
||||||
"The expression of evaluation `{expr2}` took too long to execute (>3000ms)"
|
"The expression of evaluation `{expr2}` took too long to execute (>10000ms)"
|
||||||
))
|
))
|
||||||
})??
|
})??
|
||||||
}
|
}
|
||||||
@@ -150,9 +148,8 @@ async fn eval(
|
|||||||
context: &mut JsRuntime,
|
context: &mut JsRuntime,
|
||||||
expr: &str,
|
expr: &str,
|
||||||
env: Vec<(String, serde_json::Value)>,
|
env: Vec<(String, serde_json::Value)>,
|
||||||
creds: Option<EvalCreds>,
|
|
||||||
by_id: Option<IdContext>,
|
by_id: Option<IdContext>,
|
||||||
base_internal_url: &str,
|
has_client: bool,
|
||||||
) -> anyhow::Result<serde_json::Value> {
|
) -> anyhow::Result<serde_json::Value> {
|
||||||
let exprs = expr
|
let exprs = expr
|
||||||
.trim()
|
.trim()
|
||||||
@@ -169,7 +166,7 @@ async fn eval(
|
|||||||
exprs.last().unwrap()
|
exprs.last().unwrap()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let (api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds {
|
let (api_code, by_id_code) = if has_client {
|
||||||
let by_id_code = if let Some(by_id) = by_id {
|
let by_id_code = if let Some(by_id) = by_id {
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
@@ -186,12 +183,12 @@ async function result_by_id(node_id) {{
|
|||||||
}}
|
}}
|
||||||
}} else {{
|
}} else {{
|
||||||
let flow_job_id = "{}";
|
let flow_job_id = "{}";
|
||||||
return await Deno.core.opAsync("op_get_id", [workspace, flow_job_id, token, base_url, node_id]);
|
return await Deno.core.opAsync("op_get_id", [ flow_job_id, node_id]);
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
async function get_result(id) {{
|
async function get_result(id) {{
|
||||||
return await Deno.core.opAsync("op_get_result", [workspace, id, token, base_url]);
|
return await Deno.core.opAsync("op_get_result", [id]);
|
||||||
}}
|
}}
|
||||||
const results = new Proxy({{}}, {{
|
const results = new Proxy({{}}, {{
|
||||||
get: function(target, name, receiver) {{
|
get: function(target, name, receiver) {{
|
||||||
@@ -222,17 +219,13 @@ const results = new Proxy({{}}, {{
|
|||||||
|
|
||||||
let api_code = format!(
|
let api_code = format!(
|
||||||
r#"
|
r#"
|
||||||
let workspace = "{workspace}";
|
|
||||||
let base_url = "{}";
|
|
||||||
let token = "{token}";
|
|
||||||
async function variable(path) {{
|
async function variable(path) {{
|
||||||
return await Deno.core.opAsync("op_variable", [workspace, path, token, base_url]);
|
return await Deno.core.opAsync("op_variable", [path]);
|
||||||
}}
|
}}
|
||||||
async function resource(path) {{
|
async function resource(path) {{
|
||||||
return await Deno.core.opAsync("op_resource", [workspace, path, token, base_url]);
|
return await Deno.core.opAsync("op_resource", [path]);
|
||||||
}}
|
}}
|
||||||
"#,
|
"#,
|
||||||
base_internal_url,
|
|
||||||
);
|
);
|
||||||
(api_code, by_id_code)
|
(api_code, by_id_code)
|
||||||
} else {
|
} else {
|
||||||
@@ -259,7 +252,7 @@ async function resource(path) {{
|
|||||||
.join(""),
|
.join(""),
|
||||||
);
|
);
|
||||||
tracing::debug!("{}", code);
|
tracing::debug!("{}", code);
|
||||||
let global = context.execute_script("<anon>", &code)?;
|
let global = context.execute_script("<anon>", code)?;
|
||||||
let global = context.resolve_value(global).await?;
|
let global = context.resolve_value(global).await?;
|
||||||
|
|
||||||
let scope = &mut context.handle_scope();
|
let scope = &mut context.handle_scope();
|
||||||
@@ -281,59 +274,83 @@ async function resource(path) {{
|
|||||||
|
|
||||||
// TODO: Can we a) share the api configuration here somehow or b) just implement this natively in deno, via the deno client?
|
// TODO: Can we a) share the api configuration here somehow or b) just implement this natively in deno, via the deno client?
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_variable(args: Vec<String>) -> Result<String, anyhow::Error> {
|
async fn op_variable(
|
||||||
let workspace = &args[0];
|
op_state: Rc<RefCell<OpState>>,
|
||||||
let path = &args[1];
|
args: Vec<String>,
|
||||||
let token = &args[2];
|
) -> Result<String, anyhow::Error> {
|
||||||
let base_url = &args[3];
|
let path = &args[0];
|
||||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
||||||
let result = client.get_variable(workspace, path, None).await?;
|
if let Some(client) = client {
|
||||||
Ok(result.into_inner().value.unwrap_or_else(|| "".to_owned()))
|
let result = client
|
||||||
|
.get_client()
|
||||||
|
.get_variable(&client.workspace, path, None)
|
||||||
|
.await?;
|
||||||
|
Ok(result.into_inner().value.unwrap_or_else(|| "".to_owned()))
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("No client found in op state");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_get_result(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
|
async fn op_get_result(
|
||||||
let workspace = &args[0];
|
op_state: Rc<RefCell<OpState>>,
|
||||||
let id = &args[1];
|
args: Vec<String>,
|
||||||
let token = &args[2];
|
) -> Result<serde_json::Value, anyhow::Error> {
|
||||||
let base_url = &args[3];
|
let id = &args[0];
|
||||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
||||||
let result = client
|
if let Some(client) = client {
|
||||||
.get_completed_job_result(workspace, &id.parse()?)
|
let result = client
|
||||||
.await?
|
.get_client()
|
||||||
.clone();
|
.get_completed_job_result(&client.workspace, &id.parse()?)
|
||||||
Ok(serde_json::json!(result))
|
.await?
|
||||||
|
.clone();
|
||||||
|
Ok(serde_json::json!(result))
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("No client found in op state");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyhow::Error> {
|
async fn op_get_id(
|
||||||
let workspace = &args[0];
|
op_state: Rc<RefCell<OpState>>,
|
||||||
let flow_job_id = &args[1];
|
args: Vec<String>,
|
||||||
let token = &args[2];
|
) -> Result<Option<serde_json::Value>, anyhow::Error> {
|
||||||
let base_url = &args[3];
|
let flow_job_id = &args[0];
|
||||||
let node_id = &args[4];
|
let node_id = &args[1];
|
||||||
|
|
||||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
||||||
let result = client
|
if let Some(client) = client {
|
||||||
.result_by_id(workspace, flow_job_id, node_id)
|
let result = client
|
||||||
.await
|
.get_client()
|
||||||
.map_or(None, |e| Some(e.into_inner()));
|
.result_by_id(&client.workspace, flow_job_id, node_id)
|
||||||
|
.await
|
||||||
Ok(result)
|
.map_or(None, |e| Some(e.into_inner()));
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("No client found in op state");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op]
|
#[op]
|
||||||
async fn op_resource(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
|
async fn op_resource(
|
||||||
let workspace = &args[0];
|
op_state: Rc<RefCell<OpState>>,
|
||||||
let path = &args[1];
|
args: Vec<String>,
|
||||||
let token = &args[2];
|
) -> Result<serde_json::Value, anyhow::Error> {
|
||||||
let base_url = &args[3];
|
let path = &args[0];
|
||||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
|
||||||
let result = client.get_resource(workspace, path).await?;
|
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
||||||
Ok(result
|
if let Some(client) = client {
|
||||||
.into_inner()
|
let result = client
|
||||||
.value
|
.get_client()
|
||||||
.unwrap_or_else(|| serde_json::json!({})))
|
.get_resource(&client.workspace, path)
|
||||||
|
.await?;
|
||||||
|
Ok(result
|
||||||
|
.into_inner()
|
||||||
|
.value
|
||||||
|
.unwrap_or_else(|| serde_json::json!({})))
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("No client found in op state");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -353,7 +370,7 @@ mod tests {
|
|||||||
let code = "value.test + params.test";
|
let code = "value.test + params.test";
|
||||||
|
|
||||||
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
||||||
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
|
let res = eval(&mut runtime, code, env, None, false).await?;
|
||||||
assert_eq!(res, json!(4));
|
assert_eq!(res, json!(4));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -366,7 +383,7 @@ mod tests {
|
|||||||
multiline template`";
|
multiline template`";
|
||||||
|
|
||||||
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
||||||
let res = eval(&mut runtime, code, env, None, None, String::new().as_str()).await?;
|
let res = eval(&mut runtime, code, env, None, false).await?;
|
||||||
assert_eq!(res, json!("my 5\nmultiline template"));
|
assert_eq!(res, json!("my 5\nmultiline template"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -379,7 +396,7 @@ multiline template`";
|
|||||||
];
|
];
|
||||||
let code = r#"params.test"#;
|
let code = r#"params.test"#;
|
||||||
|
|
||||||
let res = eval_timeout(code.to_string(), env, None, None, String::new().as_str()).await?;
|
let res = eval_timeout(code.to_string(), env, None, None).await?;
|
||||||
assert_eq!(res, json!(2));
|
assert_eq!(res, json!(2));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "RANDOM",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"weight": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "RANDOM",
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"weight": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "PREVIEW_SCRIPT",
|
|
||||||
"workspace": "demo",
|
|
||||||
"language": "deno",
|
|
||||||
"args": {},
|
|
||||||
"content": "export async function main() { return \"Hello World\"; }"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"weight": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "PREVIEW_SCRIPT",
|
|
||||||
"workspace": "demo",
|
|
||||||
"language": "python3",
|
|
||||||
"args": {},
|
|
||||||
"content": "def main(): return \"Hello World\";"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"weight": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "PREVIEW_SCRIPT",
|
|
||||||
"workspace": "demo",
|
|
||||||
"language": "go",
|
|
||||||
"args": {},
|
|
||||||
"content": "func main() { return \"Hello World\" }"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"weight": 0.5,
|
|
||||||
"action": {
|
|
||||||
"type": "PREVIEW_SCRIPT",
|
|
||||||
"workspace": "demo",
|
|
||||||
"language": "deno",
|
|
||||||
"args": {},
|
|
||||||
"content": "import { delay } from \"https://deno.land/std@0.131.0/async/delay.ts\"; export async function main() { await delay(1000); return \"Hello World\"; }"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
7
benchmarks/identity.json
Normal file
7
benchmarks/identity.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"type": "PREVIEW_SCRIPT",
|
||||||
|
"workspace": "demo",
|
||||||
|
"language": "deno",
|
||||||
|
"args": {},
|
||||||
|
"content": "export async function main() { return \"Hello World\"; }"
|
||||||
|
}
|
||||||
@@ -27,60 +27,60 @@ await new Command()
|
|||||||
"The number of workers to run at once.",
|
"The number of workers to run at once.",
|
||||||
{
|
{
|
||||||
default: 1,
|
default: 1,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-s --seconds <seconds:number>",
|
"-s --seconds <seconds:number>",
|
||||||
"How long to run the benchmark for (in seconds).",
|
"How long to run the benchmark for (in seconds).",
|
||||||
{
|
{
|
||||||
default: 30,
|
default: 30,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.option("--max <max:number>", "Maximum number of operations performed.")
|
.option("--max <max:number>", "Maximum number of operations performed.")
|
||||||
.option("-e --email <email:string>", "The email to use to login.")
|
.option("-e --email <email:string>", "The email to use to login.")
|
||||||
.option("-p --password <password:string>", "The password to use to login.")
|
.option("-p --password <password:string>", "The password to use to login.")
|
||||||
.env(
|
.env(
|
||||||
"WM_TOKEN=<token:string>",
|
"WM_TOKEN=<token:string>",
|
||||||
"The token to use when talking to the API server. Preferred over manual login."
|
"The token to use when talking to the API server. Preferred over manual login.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-t --token <token:string>",
|
"-t --token <token:string>",
|
||||||
"The token to use when talking to the API server. Preferred over manual login."
|
"The token to use when talking to the API server. Preferred over manual login.",
|
||||||
)
|
)
|
||||||
.env(
|
.env(
|
||||||
"WM_WORKSPACE=<workspace:string>",
|
"WM_WORKSPACE=<workspace:string>",
|
||||||
"The workspace to spawn scripts from."
|
"The workspace to spawn scripts from.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-w --workspace <workspace:string>",
|
"-w --workspace <workspace:string>",
|
||||||
"The workspace to spawn scripts from.",
|
"The workspace to spawn scripts from.",
|
||||||
{ default: "starter" }
|
{ default: "starter" },
|
||||||
)
|
)
|
||||||
.option("-m --metrics <metrics:string>", "The url to scrape metrics from.", {
|
.option("-m --metrics <metrics:string>", "The url to scrape metrics from.", {
|
||||||
default: "http://localhost:8001/metrics",
|
default: "http://localhost:8001/metrics",
|
||||||
})
|
})
|
||||||
.option(
|
.option(
|
||||||
"--export-json <export_json:string>",
|
"--export-json <export_json:string>",
|
||||||
"If set, exports will be into a JSON file."
|
"If set, exports will be into a JSON file.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--export-csv <export_csv:string>",
|
"--export-csv <export_csv:string>",
|
||||||
"If set, exports will be into a csv file."
|
"If set, exports will be into a csv file.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--export-histograms [histograms...:string]",
|
"--export-histograms [histograms...:string]",
|
||||||
"Mark metrics (without label) that are reported as histograms to export."
|
"Mark metrics (without label) that are reported as histograms to export.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--export-simple [simple...:string]",
|
"--export-simple [simple...:string]",
|
||||||
"Mark metrics (without label) that are reported as simple values."
|
"Mark metrics (without label) that are reported as simple values.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--maximum-throughput <maximum_throughput:number>",
|
"--maximum-throughput <maximum_throughput:number>",
|
||||||
"Maximum number of jobs/flows to start in one second.",
|
"Maximum number of jobs/flows to start in one second.",
|
||||||
{
|
{
|
||||||
default: Infinity,
|
default: Infinity,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.option("--use-flows", "Run flows instead of jobs.")
|
.option("--use-flows", "Run flows instead of jobs.")
|
||||||
.option("--custom <custom_path:string>", "Use custom actions during bench")
|
.option("--custom <custom_path:string>", "Use custom actions during bench")
|
||||||
@@ -89,11 +89,11 @@ await new Command()
|
|||||||
"The maximum time in ms to wait for jobs to complete.",
|
"The maximum time in ms to wait for jobs to complete.",
|
||||||
{
|
{
|
||||||
default: 90000,
|
default: 90000,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--continous",
|
"--continous",
|
||||||
"Run the benchmark forever. This effectively disables metric collection & exports. No zombie jobs will be tracked."
|
"Run the benchmark forever. This effectively disables metric collection & exports. No zombie jobs will be tracked.",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--histogram-buckets [buckets...:string]",
|
"--histogram-buckets [buckets...:string]",
|
||||||
@@ -114,7 +114,7 @@ await new Command()
|
|||||||
"0.01",
|
"0.01",
|
||||||
"0.005",
|
"0.005",
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.action(
|
.action(
|
||||||
async ({
|
async ({
|
||||||
@@ -162,7 +162,7 @@ await new Command()
|
|||||||
new URL("./scraper.ts", import.meta.url).href,
|
new URL("./scraper.ts", import.meta.url).href,
|
||||||
{
|
{
|
||||||
type: "module",
|
type: "module",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
metrics_worker.postMessage({
|
metrics_worker.postMessage({
|
||||||
@@ -192,8 +192,8 @@ await new Command()
|
|||||||
zombieTimeout,
|
zombieTimeout,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
4
|
4,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@@ -245,19 +245,22 @@ await new Command()
|
|||||||
const updateState = setInterval(async () => {
|
const updateState = setInterval(async () => {
|
||||||
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
|
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
|
||||||
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
||||||
const queue_length = (
|
const queue_length = (await (await fetch(
|
||||||
await windmill.JobService.listQueue({
|
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
||||||
workspace: config.workspace_id,
|
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
||||||
})
|
)).json()).database_length;
|
||||||
).length;
|
|
||||||
await Deno.stdout.write(
|
await Deno.stdout.write(
|
||||||
enc(
|
enc(
|
||||||
`elapsed: ${elapsed}/${seconds} | jobs sent: ${JSON.stringify(
|
`elapsed: ${elapsed}/${seconds} | jobs sent: ${
|
||||||
jobsSent
|
JSON.stringify(
|
||||||
)} (sum: ${sum} thr: ${(sum / elapsed).toFixed(
|
jobsSent,
|
||||||
2
|
)
|
||||||
)}) | queue: ${queue_length} \r`
|
} (sum: ${sum} thr: ${
|
||||||
)
|
(sum / elapsed).toFixed(
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}) | queue: ${queue_length} \r`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
@@ -284,7 +287,7 @@ await new Command()
|
|||||||
|
|
||||||
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
||||||
await Deno.stdout.write(
|
await Deno.stdout.write(
|
||||||
enc(" ".padStart(30) + `\rduration: ${seconds} | jobs sent: ${sum}\n`)
|
enc(" ".padStart(30) + `\rduration: ${seconds} | jobs sent: ${sum}\n`),
|
||||||
);
|
);
|
||||||
|
|
||||||
const shutdown_start = Date.now();
|
const shutdown_start = Date.now();
|
||||||
@@ -302,7 +305,7 @@ await new Command()
|
|||||||
};
|
};
|
||||||
worker.addEventListener("message", l);
|
worker.addEventListener("message", l);
|
||||||
worker.postMessage(
|
worker.postMessage(
|
||||||
Number.isSafeInteger(zombieTimeout) ? zombieTimeout : 90000
|
Number.isSafeInteger(zombieTimeout) ? zombieTimeout : 90000,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,11 +321,10 @@ await new Command()
|
|||||||
console.log("incorrect results: ", incorrect_results);
|
console.log("incorrect results: ", incorrect_results);
|
||||||
console.log(
|
console.log(
|
||||||
"queue length:",
|
"queue length:",
|
||||||
(
|
(await (await fetch(
|
||||||
await windmill.JobService.listQueue({
|
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
||||||
workspace: config.workspace_id,
|
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
||||||
})
|
)).json()).database_length,
|
||||||
).length
|
|
||||||
);
|
);
|
||||||
|
|
||||||
metrics_worker!.postMessage("stop");
|
metrics_worker!.postMessage("stop");
|
||||||
@@ -346,7 +348,7 @@ await new Command()
|
|||||||
const value = values[i]!;
|
const value = values[i]!;
|
||||||
const mean = value.reduce((acc, e) => acc + e, 0) / values.length;
|
const mean = value.reduce((acc, e) => acc + e, 0) / values.length;
|
||||||
const stdev = Math.sqrt(
|
const stdev = Math.sqrt(
|
||||||
value.reduce((acc, e) => acc + (e - mean) ** 2) / values.length
|
value.reduce((acc, e) => acc + (e - mean) ** 2) / values.length,
|
||||||
);
|
);
|
||||||
obj[name] = { mean, stdev };
|
obj[name] = { mean, stdev };
|
||||||
}
|
}
|
||||||
@@ -374,6 +376,6 @@ await new Command()
|
|||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
console.log("done");
|
console.log("done");
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.parse();
|
.parse();
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const promise = new Promise<{
|
|||||||
continous: boolean;
|
continous: boolean;
|
||||||
max_per_worker: number;
|
max_per_worker: number;
|
||||||
custom: Action | undefined;
|
custom: Action | undefined;
|
||||||
|
server: string;
|
||||||
|
token: string;
|
||||||
}>((resolve, _reject) => {
|
}>((resolve, _reject) => {
|
||||||
self.onmessage = (evt) => {
|
self.onmessage = (evt) => {
|
||||||
const sharedConfig = evt.data;
|
const sharedConfig = evt.data;
|
||||||
@@ -24,6 +26,8 @@ const promise = new Promise<{
|
|||||||
continous: sharedConfig.continous,
|
continous: sharedConfig.continous,
|
||||||
max_per_worker: sharedConfig.max_per_worker,
|
max_per_worker: sharedConfig.max_per_worker,
|
||||||
custom: sharedConfig.custom,
|
custom: sharedConfig.custom,
|
||||||
|
server: sharedConfig.server,
|
||||||
|
token: sharedConfig.token,
|
||||||
};
|
};
|
||||||
self.name = "Worker " + sharedConfig.i;
|
self.name = "Worker " + sharedConfig.i;
|
||||||
resolve(config);
|
resolve(config);
|
||||||
@@ -46,19 +50,20 @@ const updateStatusInterval = setInterval(() => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
while (cont) {
|
while (cont) {
|
||||||
const queue_length = (
|
const queue_length = (await (await fetch(
|
||||||
await windmill.JobService.listQueue({ workspace: config.workspace_id })
|
config.server + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
||||||
).length;
|
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
||||||
if (queue_length > 500) {
|
)).json()).database_length;
|
||||||
|
if (queue_length > 2500) {
|
||||||
console.log(
|
console.log(
|
||||||
`queue length: ${queue_length} > 500. waiting... `
|
`queue length: ${queue_length} > 2500. waiting... `,
|
||||||
);
|
);
|
||||||
await sleep(0.5);
|
await sleep(0.5);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(total_spawned * 1000) / (Date.now() - start_time) >
|
(total_spawned * 1000) / (Date.now() - start_time) >
|
||||||
config.per_worker_throughput
|
config.per_worker_throughput
|
||||||
) {
|
) {
|
||||||
console.log("at maximum throughput. waiting...");
|
console.log("at maximum throughput. waiting...");
|
||||||
await sleep(0.1);
|
await sleep(0.1);
|
||||||
@@ -138,13 +143,13 @@ while (outstanding.length > 0 && Date.now() < end_time) {
|
|||||||
await Deno.stdout.write(
|
await Deno.stdout.write(
|
||||||
enc(
|
enc(
|
||||||
`uuid: ${uuid}, queue length: ${
|
`uuid: ${uuid}, queue length: ${
|
||||||
(
|
(await (await fetch(
|
||||||
await windmill.JobService.listQueue({
|
config.server + "/api/w/" + config.workspace_id +
|
||||||
workspace: config.workspace_id,
|
"/jobs/queue/count",
|
||||||
})
|
{ headers: { ["Authorization"]: "Bearer " + config.token } },
|
||||||
).length
|
)).json()).database_length
|
||||||
} \r`
|
} \r`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
} else if (!config.useFlows) {
|
} else if (!config.useFlows) {
|
||||||
r = r as api.CompletedJob;
|
r = r as api.CompletedJob;
|
||||||
@@ -156,7 +161,7 @@ while (outstanding.length > 0 && Date.now() < end_time) {
|
|||||||
" != " +
|
" != " +
|
||||||
uuid +
|
uuid +
|
||||||
"job: \n" +
|
"job: \n" +
|
||||||
JSON.stringify(r, null, 2)
|
JSON.stringify(r, null, 2),
|
||||||
);
|
);
|
||||||
incorrect_results++;
|
incorrect_results++;
|
||||||
}
|
}
|
||||||
|
|||||||
130
cli/apps.ts
130
cli/apps.ts
@@ -1,12 +1,22 @@
|
|||||||
import { Any, model, property } from "./decoverto.ts";
|
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||||
|
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||||
import {
|
import {
|
||||||
AppService,
|
AppService,
|
||||||
AppWithLastVersion,
|
AppWithLastVersion,
|
||||||
colors,
|
colors,
|
||||||
|
Command,
|
||||||
|
ListableApp,
|
||||||
microdiff,
|
microdiff,
|
||||||
Policy,
|
Policy,
|
||||||
|
Table,
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import { Difference, PushDiffs, Resource, setValueByPath } from "./types.ts";
|
import {
|
||||||
|
Difference,
|
||||||
|
GlobalOptions,
|
||||||
|
PushDiffs,
|
||||||
|
Resource,
|
||||||
|
setValueByPath,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
@model()
|
@model()
|
||||||
export class AppFile implements Resource, PushDiffs {
|
export class AppFile implements Resource, PushDiffs {
|
||||||
@@ -17,7 +27,6 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
@property(Any)
|
@property(Any)
|
||||||
policy: Policy;
|
policy: Policy;
|
||||||
|
|
||||||
|
|
||||||
constructor(value: string, summary: string, policy: Policy) {
|
constructor(value: string, summary: string, policy: Policy) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.summary = summary;
|
this.summary = summary;
|
||||||
@@ -26,16 +35,20 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[],
|
diffs: Difference[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (await AppService.existsApp({ workspace, path: remotePath })) {
|
let app: AppWithLastVersion | undefined = undefined;
|
||||||
|
try {
|
||||||
|
app = await AppService.getAppByPath({ workspace, path: remotePath });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (app) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing app...`,
|
`Applying ${diffs.length} diffs to existing app... ${remotePath}`
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const changeset: {
|
const changeset: {
|
||||||
path?: string | undefined;
|
|
||||||
summary?: string | undefined;
|
summary?: string | undefined;
|
||||||
value?: any;
|
value?: any;
|
||||||
policy?: Policy | undefined;
|
policy?: Policy | undefined;
|
||||||
@@ -43,14 +56,10 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
(
|
diff.path[0] !== "value" &&
|
||||||
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
|
diff.path[0] !== "policy" &&
|
||||||
diff.path.length !== 1 ||
|
(diff.path.length !== 1 ||
|
||||||
!["path", "summary"].includes(
|
!["summary"].includes(diff.path[0] as string))
|
||||||
diff.path[0] as string,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid app diff with path " + diff.path);
|
throw new Error("Invalid app diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
@@ -61,8 +70,21 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some((v) =>
|
if (
|
||||||
v !== null && typeof v !== "undefined"
|
(!changeset?.policy ||
|
||||||
|
JSON.stringify(changeset?.policy) == JSON.stringify(app.policy)) &&
|
||||||
|
(!changeset?.value ||
|
||||||
|
JSON.stringify(changeset?.value) == JSON.stringify(app.value)) &&
|
||||||
|
(!changeset?.summary || changeset.summary == app.summary)
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
colors.yellow(`No changes to push for app ${remotePath}, skipping`)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChanges = Object.values(changeset).some(
|
||||||
|
(v) => v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
@@ -75,6 +97,7 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(colors.yellow.bold("Creating new app..."));
|
console.log(colors.yellow.bold("Creating new app..."));
|
||||||
|
|
||||||
await AppService.createApp({
|
await AppService.createApp({
|
||||||
workspace,
|
workspace,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
@@ -87,19 +110,70 @@ export class AppFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
let existing: AppWithLastVersion | undefined;
|
|
||||||
try {
|
|
||||||
existing = await AppService.getAppByPath({
|
|
||||||
workspace: workspace,
|
|
||||||
path: remotePath,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
existing = undefined;
|
|
||||||
}
|
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
microdiff({}, this, { cyclesFix: false })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function list(opts: GlobalOptions) {
|
||||||
|
const workspace = await resolveWorkspace(opts);
|
||||||
|
await requireLogin(opts);
|
||||||
|
|
||||||
|
let page = 0;
|
||||||
|
const perPage = 10;
|
||||||
|
const total: ListableApp[] = [];
|
||||||
|
while (true) {
|
||||||
|
const res = await AppService.listApps({
|
||||||
|
workspace: workspace.workspaceId,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
});
|
||||||
|
page += 1;
|
||||||
|
total.push(...res);
|
||||||
|
if (res.length < perPage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Table()
|
||||||
|
.header(["path", "summary"])
|
||||||
|
.padding(2)
|
||||||
|
.border(true)
|
||||||
|
.body(total.map((x) => [x.path, x.summary]))
|
||||||
|
.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function push(opts: GlobalOptions, filePath: string) {
|
||||||
|
const remotePath = filePath.split(".")[0];
|
||||||
|
if (!validatePath(remotePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workspace = await resolveWorkspace(opts);
|
||||||
|
await requireLogin(opts);
|
||||||
|
|
||||||
|
await pushApp(filePath, workspace.workspaceId, remotePath);
|
||||||
|
console.log(colors.bold.underline.green("App pushed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pushApp(
|
||||||
|
filePath: string,
|
||||||
|
workspace: string,
|
||||||
|
remotePath: string
|
||||||
|
) {
|
||||||
|
const data = decoverto
|
||||||
|
.type(AppFile)
|
||||||
|
.rawToInstance(await Deno.readTextFile(filePath));
|
||||||
|
await data.push(workspace, remotePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = new Command()
|
||||||
|
.description("app related commands")
|
||||||
|
.action(list as any)
|
||||||
|
.command("push", "push a local app ")
|
||||||
|
.arguments("<file_path:file>")
|
||||||
|
.action(push as any);
|
||||||
|
|
||||||
|
export default command;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type Context = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function tryResolveWorkspace(
|
async function tryResolveWorkspace(
|
||||||
opts: GlobalOptions,
|
opts: GlobalOptions
|
||||||
): Promise<
|
): Promise<
|
||||||
{ isError: false; value: Workspace } | { isError: true; error: string }
|
{ isError: false; value: Workspace } | { isError: true; error: string }
|
||||||
> {
|
> {
|
||||||
@@ -49,17 +49,20 @@ async function tryResolveWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveWorkspace(
|
export async function resolveWorkspace(
|
||||||
opts: GlobalOptions,
|
opts: GlobalOptions
|
||||||
): Promise<Workspace> {
|
): Promise<Workspace> {
|
||||||
const res = await tryResolveWorkspace(opts);
|
const res = await tryResolveWorkspace(opts);
|
||||||
if (res.isError) {
|
if (res.isError) {
|
||||||
|
console.log(colors.red.bold(res.error));
|
||||||
return Deno.exit(-1);
|
return Deno.exit(-1);
|
||||||
} else {
|
} else {
|
||||||
return res.value;
|
return res.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo> {
|
export async function requireLogin(
|
||||||
|
opts: GlobalOptions
|
||||||
|
): Promise<GlobalUserInfo> {
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
let token = await tryGetLoginInfo(opts);
|
let token = await tryGetLoginInfo(opts);
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo>
|
|||||||
return await UserService.globalWhoami();
|
return await UserService.globalWhoami();
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
console.log(
|
||||||
"! Could not reach API given existing credentials. Attempting to reauth...",
|
"! Could not reach API given existing credentials. Attempting to reauth..."
|
||||||
);
|
);
|
||||||
const newToken = await loginInteractive(workspace.remote);
|
const newToken = await loginInteractive(workspace.remote);
|
||||||
if (!newToken) {
|
if (!newToken) {
|
||||||
@@ -85,14 +88,14 @@ export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo>
|
|||||||
|
|
||||||
setClient(
|
setClient(
|
||||||
token,
|
token,
|
||||||
workspace.remote.substring(0, workspace.remote.length - 1),
|
workspace.remote.substring(0, workspace.remote.length - 1)
|
||||||
);
|
);
|
||||||
return await UserService.globalWhoami();
|
return await UserService.globalWhoami();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tryResolveVersion(
|
export async function tryResolveVersion(
|
||||||
opts: GlobalOptions,
|
opts: GlobalOptions
|
||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
if ((opts as any).__cache_version) {
|
if ((opts as any).__cache_version) {
|
||||||
return (opts as any).__cache_version;
|
return (opts as any).__cache_version;
|
||||||
@@ -102,41 +105,24 @@ export async function tryResolveVersion(
|
|||||||
if (workspaceRes.isError) return undefined;
|
if (workspaceRes.isError) return undefined;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
|
new URL(new URL(workspaceRes.value.remote).origin + "/api/version")
|
||||||
);
|
);
|
||||||
const version = await response.text();
|
const version = await response.text();
|
||||||
try {
|
try {
|
||||||
return Number.parseInt(
|
return Number.parseInt(
|
||||||
version.split("-", 1)[0].replaceAll(".", "").replace("v", ""),
|
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validatePath(
|
export function validatePath(path: string): boolean {
|
||||||
opts: GlobalOptions,
|
if (!(path.startsWith("g") || path.startsWith("u") || path.startsWith("f"))) {
|
||||||
path: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const backendVersion = await tryResolveVersion(opts);
|
|
||||||
if (path.startsWith("f")) {
|
|
||||||
if (!backendVersion || backendVersion >= 1550) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(path.startsWith("g") ||
|
|
||||||
path.startsWith("u"))
|
|
||||||
) {
|
|
||||||
console.log(
|
console.log(
|
||||||
colors.red(
|
colors.red(
|
||||||
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/...",
|
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/..."
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ export { passwordGenerator } from "https://deno.land/x/password_generator@latest
|
|||||||
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
||||||
export * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";
|
export * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";
|
||||||
export { default as Murmurhash3 } from "https://deno.land/x/murmurhash@v1.0.0/mod.ts";
|
export { default as Murmurhash3 } from "https://deno.land/x/murmurhash@v1.0.0/mod.ts";
|
||||||
export {
|
export { default as microdiff } from "https://deno.land/x/microdiff@v1.3.1/index.ts";
|
||||||
default as microdiff,
|
|
||||||
} from "https://deno.land/x/microdiff@v1.3.1/index.ts";
|
|
||||||
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
|
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";
|
||||||
export { default as gitignore_parser } from "npm:gitignore-parser";
|
export { default as gitignore_parser } from "npm:gitignore-parser";
|
||||||
export { default as JSZip } from "npm:jszip@3.7.1";
|
export { default as JSZip } from "npm:jszip@3.7.1";
|
||||||
|
|||||||
70
cli/flow.ts
70
cli/flow.ts
@@ -20,7 +20,6 @@ import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
|||||||
import { resolve, track_job } from "./script.ts";
|
import { resolve, track_job } from "./script.ts";
|
||||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||||
|
|
||||||
|
|
||||||
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
|
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
|
||||||
@model()
|
@model()
|
||||||
export class FlowFile implements Resource, PushDiffs {
|
export class FlowFile implements Resource, PushDiffs {
|
||||||
@@ -40,7 +39,7 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[],
|
diffs: Difference[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
await FlowService.existsFlowByPath({
|
await FlowService.existsFlowByPath({
|
||||||
@@ -50,8 +49,8 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`,
|
`Applying ${diffs.length} diffs to existing flow... ${remotePath}`
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Make these optional in backend (not path ofc)
|
// TODO: Make these optional in backend (not path ofc)
|
||||||
@@ -66,14 +65,11 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
(
|
diff.path[0] !== "value" &&
|
||||||
diff.path[0] !== "value" && (
|
(diff.path.length !== 1 ||
|
||||||
diff.path.length !== 1 ||
|
!["summary", "description", "schema"].includes(
|
||||||
!["summary", "description", "schema"].includes(
|
diff.path[0] as string
|
||||||
diff.path[0] as string,
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid flow diff with path " + diff.path);
|
throw new Error("Invalid flow diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
@@ -83,8 +79,8 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
setValueByPath(changeset, diff.path, null);
|
setValueByPath(changeset, diff.path, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hasChanges = Object.values(changeset).some((v) =>
|
const hasChanges = Object.values(changeset).some(
|
||||||
v !== null && typeof v !== "undefined"
|
(v) => v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
@@ -93,7 +89,8 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
const update = {
|
const update = {
|
||||||
...changeset,
|
...changeset,
|
||||||
...base_changeset,
|
...base_changeset,
|
||||||
}
|
};
|
||||||
|
|
||||||
await FlowService.updateFlow({
|
await FlowService.updateFlow({
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
@@ -114,20 +111,10 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
let remote: Flow | undefined;
|
|
||||||
try {
|
|
||||||
remote = await FlowService.getFlowByPath({
|
|
||||||
workspace,
|
|
||||||
path: remotePath,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
remote = undefined;
|
|
||||||
}
|
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff(remote ?? {}, this, { cyclesFix: false }),
|
microdiff({}, this, { cyclesFix: false })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,24 +122,24 @@ export class FlowFile implements Resource, PushDiffs {
|
|||||||
type Options = GlobalOptions;
|
type Options = GlobalOptions;
|
||||||
|
|
||||||
async function push(opts: Options, filePath: string, remotePath: string) {
|
async function push(opts: Options, filePath: string, remotePath: string) {
|
||||||
if (!await validatePath(opts, remotePath)) {
|
if (!validatePath(remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
await pushFlow(filePath, workspace.remote, remotePath);
|
await pushFlow(filePath, workspace.workspaceId, remotePath);
|
||||||
console.log(colors.bold.underline.green("Flow pushed"));
|
console.log(colors.bold.underline.green("Flow pushed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushFlow(
|
export async function pushFlow(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string
|
||||||
) {
|
) {
|
||||||
const data = decoverto.type(FlowFile).rawToInstance(
|
const data = decoverto
|
||||||
await Deno.readTextFile(filePath),
|
.type(FlowFile)
|
||||||
);
|
.rawToInstance(await Deno.readTextFile(filePath));
|
||||||
await data.push(workspace, remotePath);
|
await data.push(workspace, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,13 +168,7 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
|||||||
.header(["path", "summary", "edited by"])
|
.header(["path", "summary", "edited by"])
|
||||||
.padding(2)
|
.padding(2)
|
||||||
.border(true)
|
.border(true)
|
||||||
.body(
|
.body(total.map((x) => [x.path, x.summary, x.edited_by]))
|
||||||
total.map((x) => [
|
|
||||||
x.path,
|
|
||||||
x.summary,
|
|
||||||
x.edited_by,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.render();
|
.render();
|
||||||
}
|
}
|
||||||
async function run(
|
async function run(
|
||||||
@@ -195,14 +176,13 @@ async function run(
|
|||||||
data?: string;
|
data?: string;
|
||||||
silent: boolean;
|
silent: boolean;
|
||||||
},
|
},
|
||||||
path: string,
|
path: string
|
||||||
) {
|
) {
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
const input = opts.data ? await resolve(opts.data) : {};
|
const input = opts.data ? await resolve(opts.data) : {};
|
||||||
|
|
||||||
|
|
||||||
const id = await JobService.runFlowByPath({
|
const id = await JobService.runFlowByPath({
|
||||||
workspace: workspace.workspaceId,
|
workspace: workspace.workspaceId,
|
||||||
path,
|
path,
|
||||||
@@ -237,7 +217,7 @@ async function run(
|
|||||||
|
|
||||||
if (!opts.silent) {
|
if (!opts.silent) {
|
||||||
console.log(colors.green.underline.bold("Flow ran to completion"));
|
console.log(colors.green.underline.bold("Flow ran to completion"));
|
||||||
console.log()
|
console.log();
|
||||||
}
|
}
|
||||||
const jobInfo = await JobService.getCompletedJob({
|
const jobInfo = await JobService.getCompletedJob({
|
||||||
workspace: workspace.workspaceId,
|
workspace: workspace.workspaceId,
|
||||||
@@ -252,7 +232,7 @@ const command = new Command()
|
|||||||
.action(list as any)
|
.action(list as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local flow spec. This overrides any remote versions.",
|
"push a local flow spec. This overrides any remote versions."
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
.action(push as any)
|
.action(push as any)
|
||||||
@@ -260,11 +240,11 @@ const command = new Command()
|
|||||||
.arguments("<path:string>")
|
.arguments("<path:string>")
|
||||||
.option(
|
.option(
|
||||||
"-d --data <data:string>",
|
"-d --data <data:string>",
|
||||||
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-.",
|
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-s --silent",
|
"-s --silent",
|
||||||
"Do not ouput anything other then the final output. Useful for scripting.",
|
"Do not ouput anything other then the final output. Useful for scripting."
|
||||||
)
|
)
|
||||||
.action(run as any);
|
.action(run as any);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
|
import { colors, Command, FolderService, microdiff } from "./deps.ts";
|
||||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||||
import {
|
import {
|
||||||
Difference,
|
Difference,
|
||||||
@@ -20,8 +20,16 @@ import {
|
|||||||
export class FolderFile implements Resource, PushDiffs {
|
export class FolderFile implements Resource, PushDiffs {
|
||||||
@property(array(() => String))
|
@property(array(() => String))
|
||||||
owners: Array<string> | undefined;
|
owners: Array<string> | undefined;
|
||||||
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
|
@property(
|
||||||
|
map(
|
||||||
|
() => String,
|
||||||
|
() => Boolean,
|
||||||
|
{ shape: MapShape.Object }
|
||||||
|
)
|
||||||
|
)
|
||||||
extra_perms: Map<string, boolean> | undefined;
|
extra_perms: Map<string, boolean> | undefined;
|
||||||
|
@property(() => String)
|
||||||
|
display_name: string | undefined;
|
||||||
|
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
if (remotePath.startsWith("/")) {
|
if (remotePath.startsWith("/")) {
|
||||||
@@ -31,23 +39,17 @@ export class FolderFile implements Resource, PushDiffs {
|
|||||||
remotePath = remotePath.substring(2);
|
remotePath = remotePath.substring(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
let existing: Folder | undefined;
|
|
||||||
try {
|
|
||||||
existing = await FolderService.getFolder({ workspace, name: remotePath });
|
|
||||||
} catch {
|
|
||||||
existing = undefined;
|
|
||||||
}
|
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
microdiff({}, this, { cyclesFix: false })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[],
|
diffs: Difference[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (remotePath.startsWith("/")) {
|
if (remotePath.startsWith("/")) {
|
||||||
remotePath = remotePath.substring(1);
|
remotePath = remotePath.substring(1);
|
||||||
@@ -59,29 +61,34 @@ export class FolderFile implements Resource, PushDiffs {
|
|||||||
// TODO: Support this in backend
|
// TODO: Support this in backend
|
||||||
let exists: boolean;
|
let exists: boolean;
|
||||||
try {
|
try {
|
||||||
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
|
exists = !!(await FolderService.getFolder({
|
||||||
|
workspace,
|
||||||
|
name: remotePath,
|
||||||
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
exists = false;
|
exists = false;
|
||||||
}
|
}
|
||||||
if (exists) {
|
if (exists) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing folder...`,
|
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeset: {
|
const changeset: {
|
||||||
owners?: string[] | undefined;
|
owners?: string[] | undefined;
|
||||||
extra_perms?: any;
|
extra_perms?: any;
|
||||||
|
display_name?: string | undefined;
|
||||||
} = {};
|
} = {};
|
||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
(
|
(diff.path.length !== 1 ||
|
||||||
diff.path.length !== 1 ||
|
!["owners", "extra_perms", "display_name"].includes(
|
||||||
!["owners", "extra_perms"].includes(diff.path[0] as string)
|
diff.path[0] as string
|
||||||
)
|
))
|
||||||
) {
|
) {
|
||||||
|
console.log(diff.path);
|
||||||
throw new Error("Invalid folder diff with path " + diff.path);
|
throw new Error("Invalid folder diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||||
@@ -91,18 +98,27 @@ export class FolderFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some((v) =>
|
const hasChanges = Object.values(changeset).some(
|
||||||
v !== null && typeof v !== "undefined"
|
(v) => v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
await FolderService.updateFolder({
|
await FolderService.updateFolder({
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
name: remotePath,
|
name: remotePath,
|
||||||
requestBody: changeset,
|
requestBody: {
|
||||||
});
|
...changeset,
|
||||||
|
extra_perms: changeset.extra_perms
|
||||||
|
? Object.fromEntries(this.extra_perms?.entries() ?? [])
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(colors.red.bold(e.body));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
|
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
|
||||||
await FolderService.createFolder({
|
await FolderService.createFolder({
|
||||||
@@ -121,7 +137,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
|||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
if (!await validatePath(opts, remotePath)) {
|
if (!validatePath(remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +155,11 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
|||||||
export async function pushFolder(
|
export async function pushFolder(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
remotePath: string,
|
remotePath: string
|
||||||
) {
|
) {
|
||||||
const data = decoverto.type(FolderFile).rawToInstance(
|
const data = decoverto
|
||||||
await Deno.readTextFile(filePath),
|
.type(FolderFile)
|
||||||
);
|
.rawToInstance(await Deno.readTextFile(filePath));
|
||||||
data.push(workspace, remotePath);
|
data.push(workspace, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +167,7 @@ const command = new Command()
|
|||||||
.description("resource related commands")
|
.description("resource related commands")
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local folder spec. This overrides any remote versions.",
|
"push a local folder spec. This overrides any remote versions."
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
.action(push as any);
|
.action(push as any);
|
||||||
|
|||||||
22
cli/main.ts
22
cli/main.ts
@@ -1,5 +1,11 @@
|
|||||||
import { Command, CompletionsCommand, DenoLandProvider, UpgradeCommand } from "./deps.ts";
|
import {
|
||||||
|
Command,
|
||||||
|
CompletionsCommand,
|
||||||
|
DenoLandProvider,
|
||||||
|
UpgradeCommand,
|
||||||
|
} from "./deps.ts";
|
||||||
import flow from "./flow.ts";
|
import flow from "./flow.ts";
|
||||||
|
import app from "./apps.ts";
|
||||||
import script from "./script.ts";
|
import script from "./script.ts";
|
||||||
import workspace from "./workspace.ts";
|
import workspace from "./workspace.ts";
|
||||||
import resource from "./resource.ts";
|
import resource from "./resource.ts";
|
||||||
@@ -13,21 +19,21 @@ import sync from "./sync.ts";
|
|||||||
import { tryResolveVersion } from "./context.ts";
|
import { tryResolveVersion } from "./context.ts";
|
||||||
import { GlobalOptions } from "./types.ts";
|
import { GlobalOptions } from "./types.ts";
|
||||||
|
|
||||||
const VERSION = "v1.74.2";
|
export const VERSION = "v1.87.0";
|
||||||
|
|
||||||
let command: any = new Command()
|
let command: any = new Command()
|
||||||
.name("wmill")
|
.name("wmill")
|
||||||
.description("A simple CLI tool for windmill.")
|
.description("A simple CLI tool for windmill.")
|
||||||
.action(() => command.showHelp())
|
|
||||||
.globalOption(
|
.globalOption(
|
||||||
"--workspace <workspace:string>",
|
"--workspace <workspace:string>",
|
||||||
"Specify the target workspace. This overrides the default workspace.",
|
"Specify the target workspace. This overrides the default workspace."
|
||||||
)
|
)
|
||||||
.globalOption(
|
.globalOption(
|
||||||
"--token <token:string>",
|
"--token <token:string>",
|
||||||
"Specify an API token. This will override any stored token.",
|
"Specify an API token. This will override any stored token."
|
||||||
)
|
)
|
||||||
.version(VERSION)
|
.version(VERSION)
|
||||||
|
.command("app", app)
|
||||||
.command("flow", flow)
|
.command("flow", flow)
|
||||||
.command("script", script)
|
.command("script", script)
|
||||||
.command("workspace", workspace)
|
.command("workspace", workspace)
|
||||||
@@ -59,14 +65,12 @@ let command: any = new Command()
|
|||||||
"--unstable",
|
"--unstable",
|
||||||
],
|
],
|
||||||
provider: new DenoLandProvider({ name: "wmill" }),
|
provider: new DenoLandProvider({ name: "wmill" }),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.command("completions", new CompletionsCommand());
|
.command("completions", new CompletionsCommand());
|
||||||
|
|
||||||
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
|
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
|
||||||
command = command
|
command = command.command("push", push).command("pull", pull);
|
||||||
.command("push", push)
|
|
||||||
.command("pull", pull);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
22
cli/pull.ts
22
cli/pull.ts
@@ -5,25 +5,27 @@ import { Workspace } from "./workspace.ts";
|
|||||||
|
|
||||||
export async function downloadZip(
|
export async function downloadZip(
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
|
plainSecrets: boolean | undefined
|
||||||
): Promise<JSZip | undefined> {
|
): Promise<JSZip | undefined> {
|
||||||
const requestHeaders: HeadersInit = new Headers();
|
const requestHeaders: HeadersInit = new Headers();
|
||||||
requestHeaders.set("Authorization", "Bearer " + workspace.token);
|
requestHeaders.set("Authorization", "Bearer " + workspace.token);
|
||||||
requestHeaders.set("Content-Type", "application/octet-stream");
|
requestHeaders.set("Content-Type", "application/octet-stream");
|
||||||
|
|
||||||
const zipResponse = await fetch(
|
const zipResponse = await fetch(
|
||||||
workspace.remote + "api/w/" + workspace.workspaceId +
|
workspace.remote +
|
||||||
"/workspaces/tarball?archive_type=zip",
|
"api/w/" +
|
||||||
|
workspace.workspaceId +
|
||||||
|
"/workspaces/tarball?archive_type=zip&plain_secret=" +
|
||||||
|
(plainSecrets ?? false),
|
||||||
{
|
{
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!zipResponse.ok) {
|
if (!zipResponse.ok) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red(
|
colors.red("Failed to request tarball from API " + zipResponse.statusText)
|
||||||
"Failed to request tarball from API " + zipResponse.statusText,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
throw new Error(await zipResponse.text());
|
throw new Error(await zipResponse.text());
|
||||||
}
|
}
|
||||||
@@ -33,18 +35,18 @@ export async function downloadZip(
|
|||||||
|
|
||||||
async function stub(
|
async function stub(
|
||||||
_opts: GlobalOptions & { override: boolean },
|
_opts: GlobalOptions & { override: boolean },
|
||||||
_dir: string,
|
_dir: string
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red.underline(
|
colors.red.underline(
|
||||||
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.',
|
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.'
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new Command()
|
const command = new Command()
|
||||||
.description(
|
.description(
|
||||||
"Pull all definitions in the current workspace from the API and write them to disk.",
|
"Pull all definitions in the current workspace from the API and write them to disk."
|
||||||
)
|
)
|
||||||
.arguments("<dir:string>")
|
.arguments("<dir:string>")
|
||||||
.action(stub as any);
|
.action(stub as any);
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
EditResourceType,
|
EditResourceType,
|
||||||
microdiff,
|
microdiff,
|
||||||
ResourceService,
|
ResourceService,
|
||||||
ResourceType,
|
|
||||||
Table,
|
Table,
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||||
@@ -26,19 +25,10 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
|||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
let existing: ResourceType | undefined;
|
await this.pushDiffs(
|
||||||
try {
|
|
||||||
existing = await ResourceService.getResourceType({
|
|
||||||
workspace,
|
|
||||||
path: remotePath,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
existing = undefined;
|
|
||||||
}
|
|
||||||
this.pushDiffs(
|
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
microdiff({}, this, { cyclesFix: false }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +55,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow(
|
colors.yellow.bold(
|
||||||
`Applying ${diffs.length} diffs to existing resource type...`,
|
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const changeset: EditResourceType = {};
|
const changeset: EditResourceType = {};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
async pushDiffs(
|
async pushDiffs(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[],
|
diffs: Difference[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
await ResourceService.existsResource({
|
await ResourceService.existsResource({
|
||||||
@@ -43,7 +43,9 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
|
colors.yellow.bold(
|
||||||
|
`Applying ${diffs.length} diffs to existing resource... ${remotePath}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeset: EditResource = {
|
const changeset: EditResource = {
|
||||||
@@ -51,22 +53,18 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
};
|
};
|
||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (diff.path[0] === "is_oauth") {
|
if (diff.path[0] === "is_oauth") {
|
||||||
console.log(
|
//is_oauth is not updatable
|
||||||
colors.yellow(
|
|
||||||
"! is_oauth has been removed in newer versions. Ignoring.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
(
|
diff.path[0] !== "value" &&
|
||||||
diff.path[0] !== "value" && (
|
(diff.path.length !== 1 || diff.path[0] !== "description") &&
|
||||||
diff.path.length !== 1 ||
|
diff.path[0] !== "resource_type"
|
||||||
diff.path[0] !== "description"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
|
console.log(
|
||||||
|
colors.red("Invalid variable diff with path " + diff.path)
|
||||||
|
);
|
||||||
throw new Error("Invalid folder diff with path " + diff.path);
|
throw new Error("Invalid folder diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||||
@@ -76,8 +74,8 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some((v) =>
|
const hasChanges = Object.values(changeset).some(
|
||||||
v !== null && typeof v !== "undefined"
|
(v) => v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
@@ -92,8 +90,8 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
if (typeof this.is_oauth !== "undefined") {
|
if (typeof this.is_oauth !== "undefined") {
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow(
|
colors.yellow(
|
||||||
"! is_oauth has been removed in newer versions. Ignoring.",
|
"! is_oauth has been removed in newer versions. Ignoring."
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,19 +108,10 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(workspace: string, remotePath: string): Promise<void> {
|
||||||
let existing: Resource | undefined;
|
|
||||||
try {
|
|
||||||
existing = await ResourceService.getResource({
|
|
||||||
workspace,
|
|
||||||
path: remotePath,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
existing = undefined;
|
|
||||||
}
|
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
microdiff({}, this, { cyclesFix: false })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,11 +119,11 @@ export class ResourceFile implements Resource2, PushDiffs {
|
|||||||
export async function pushResource(
|
export async function pushResource(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
remotePath: string,
|
remotePath: string
|
||||||
) {
|
) {
|
||||||
const data = decoverto.type(ResourceFile).rawToInstance(
|
const data = decoverto
|
||||||
await Deno.readTextFile(filePath),
|
.type(ResourceFile)
|
||||||
);
|
.rawToInstance(await Deno.readTextFile(filePath));
|
||||||
await data.push(workspace, remotePath);
|
await data.push(workspace, remotePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +132,7 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
|
|||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
if (!await validatePath(opts, remotePath)) {
|
if (!validatePath(remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +179,7 @@ const command = new Command()
|
|||||||
.action(list as any)
|
.action(list as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local resource spec. This overrides any remote versions.",
|
"push a local resource spec. This overrides any remote versions."
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
.action(push as any);
|
.action(push as any);
|
||||||
|
|||||||
235
cli/script.ts
235
cli/script.ts
@@ -32,8 +32,11 @@ export class ScriptFile {
|
|||||||
if (data == null) return data;
|
if (data == null) return data;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
data === "script" || data === "failure" || data === "trigger" ||
|
data === "script" ||
|
||||||
data === "command" || data === "approvial"
|
data === "failure" ||
|
||||||
|
data === "trigger" ||
|
||||||
|
data === "command" ||
|
||||||
|
data === "approvial"
|
||||||
) {
|
) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -42,6 +45,7 @@ export class ScriptFile {
|
|||||||
},
|
},
|
||||||
toPlain: (data) => data,
|
toPlain: (data) => data,
|
||||||
})
|
})
|
||||||
|
@property(() => String)
|
||||||
kind?: "script" | "failure" | "trigger" | "command" | "approval";
|
kind?: "script" | "failure" | "trigger" | "command" | "approval";
|
||||||
|
|
||||||
constructor(summary: string, description: string) {
|
constructor(summary: string, description: string) {
|
||||||
@@ -51,14 +55,11 @@ export class ScriptFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PushOptions = GlobalOptions;
|
type PushOptions = GlobalOptions;
|
||||||
async function push(
|
async function push(opts: PushOptions, filePath: string) {
|
||||||
opts: PushOptions,
|
|
||||||
filePath: string,
|
|
||||||
remotePath: string,
|
|
||||||
contentPath?: string,
|
|
||||||
) {
|
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
if (!await validatePath(opts, remotePath)) {
|
const remotePath = filePath.split(".")[0];
|
||||||
|
|
||||||
|
if (!validatePath(remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,89 +67,137 @@ async function push(
|
|||||||
if (!fstat.isFile) {
|
if (!fstat.isFile) {
|
||||||
throw new Error("file path must refer to a file.");
|
throw new Error("file path must refer to a file.");
|
||||||
}
|
}
|
||||||
if (!contentPath) {
|
let contentPath: string;
|
||||||
|
let metaPath: string | undefined;
|
||||||
|
if (filePath.endsWith(".script.json")) {
|
||||||
|
metaPath = filePath;
|
||||||
contentPath = await findContentFile(filePath);
|
contentPath = await findContentFile(filePath);
|
||||||
} else {
|
} else {
|
||||||
const fstat = await Deno.stat(filePath);
|
contentPath = filePath;
|
||||||
if (!fstat.isFile) {
|
metaPath = undefined;
|
||||||
throw new Error("content path must refer to a file.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
await pushScript(filePath, contentPath, workspace.workspaceId, remotePath);
|
await pushScript(metaPath, contentPath, workspace.workspaceId, remotePath);
|
||||||
console.log(colors.bold.underline.green(`Script ${remotePath} pushed`));
|
console.log(colors.bold.underline.green(`Script ${remotePath} pushed`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleScriptMetadata(path: string, workspace: string, alreadySynced: string[]): Promise<boolean> {
|
export async function handleScriptMetadata(
|
||||||
|
path: string,
|
||||||
|
workspace: string,
|
||||||
|
alreadySynced: string[]
|
||||||
|
): Promise<boolean> {
|
||||||
if (path.endsWith(".script.json")) {
|
if (path.endsWith(".script.json")) {
|
||||||
const contentPath = await findContentFile(path)
|
const contentPath = await findContentFile(path);
|
||||||
return handleFile(contentPath, await Deno.readTextFile(contentPath), workspace, alreadySynced)
|
return handleFile(
|
||||||
|
contentPath,
|
||||||
|
await Deno.readTextFile(contentPath),
|
||||||
|
workspace,
|
||||||
|
alreadySynced
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleFile(path: string, content: string, workspace: string, alreadySynced: string[]): Promise<boolean> {
|
export async function handleFile(
|
||||||
if (path.endsWith(".ts") || path.endsWith(".py") || path.endsWith(".go") || path.endsWith(".sh")) {
|
path: string,
|
||||||
|
content: string,
|
||||||
|
workspace: string,
|
||||||
|
alreadySynced: string[]
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (
|
||||||
|
path.endsWith(".ts") ||
|
||||||
|
path.endsWith(".py") ||
|
||||||
|
path.endsWith(".go") ||
|
||||||
|
path.endsWith(".sh")
|
||||||
|
) {
|
||||||
if (alreadySynced.includes(path)) {
|
if (alreadySynced.includes(path)) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
alreadySynced.push(path)
|
alreadySynced.push(path);
|
||||||
const remotePath = path.substring(0, path.length - 3);
|
const remotePath = path.substring(0, path.length - 3);
|
||||||
const metaPath = remotePath + ".script.json";
|
const metaPath = remotePath + ".script.json";
|
||||||
let typed = undefined
|
let typed = undefined;
|
||||||
try {
|
try {
|
||||||
await Deno.stat(metaPath)
|
await Deno.stat(metaPath);
|
||||||
typed = JSON.parse(await Deno.readTextFile(metaPath))
|
typed = JSON.parse(await Deno.readTextFile(metaPath));
|
||||||
typed = decoverto.type(ScriptFile).plainToInstance(typed);
|
typed = decoverto.type(ScriptFile).plainToInstance(typed);
|
||||||
} catch { }
|
} catch {}
|
||||||
const language = inferContentTypeFromFilePath(path);
|
const language = inferContentTypeFromFilePath(path);
|
||||||
|
|
||||||
|
let remote = undefined;
|
||||||
try {
|
try {
|
||||||
const remote = await ScriptService.getScriptByPath({
|
remote = await ScriptService.getScriptByPath({
|
||||||
workspace,
|
workspace,
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
});
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (remote) {
|
||||||
|
if (content === remote.content) {
|
||||||
|
if (
|
||||||
|
typed == undefined ||
|
||||||
|
(typed.description === remote.description &&
|
||||||
|
typed.summary === remote.summary &&
|
||||||
|
typed.is_template === remote.is_template &&
|
||||||
|
typed.kind == remote.kind &&
|
||||||
|
!remote.archived &&
|
||||||
|
remote?.lock == typed.lock?.join("\n") &&
|
||||||
|
JSON.stringify(typed.schema) == JSON.stringify(remote.schema))
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
colors.yellow(
|
||||||
|
`No change to push for script ${remotePath}, skipping`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await ScriptService.createScript({
|
await ScriptService.createScript({
|
||||||
workspace,
|
workspace,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content,
|
content,
|
||||||
description: typed.description,
|
description: typed?.description ?? "",
|
||||||
language,
|
language,
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
summary: typed.summary,
|
summary: typed?.summary ?? "",
|
||||||
is_template: typed.is_template,
|
is_template: typed?.is_template,
|
||||||
kind: typed.kind,
|
kind: typed?.kind,
|
||||||
lock: typed.lock,
|
lock: typed?.lock,
|
||||||
parent_hash: remote.hash,
|
parent_hash: remote.hash,
|
||||||
schema: typed.schema,
|
schema: typed?.schema,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`))
|
|
||||||
} catch {
|
console.log(
|
||||||
|
colors.yellow.bold(`Creating script with a parent ${remotePath}`)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
// no parent hash
|
// no parent hash
|
||||||
await ScriptService.createScript({
|
await ScriptService.createScript({
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content,
|
content,
|
||||||
description: typed.description,
|
description: typed?.description ?? "",
|
||||||
language,
|
language,
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
summary: typed.summary,
|
summary: typed?.summary ?? "",
|
||||||
is_template: typed.is_template,
|
is_template: typed?.is_template,
|
||||||
kind: typed.kind,
|
kind: typed?.kind,
|
||||||
lock: typed.lock,
|
lock: typed?.lock,
|
||||||
parent_hash: undefined,
|
parent_hash: undefined,
|
||||||
schema: typed.schema,
|
schema: typed?.schema,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`))
|
console.log(
|
||||||
|
colors.yellow.bold(`Creating script without parent ${remotePath}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findContentFile(filePath: string) {
|
export async function findContentFile(filePath: string) {
|
||||||
@@ -167,7 +216,7 @@ export async function findContentFile(filePath: string) {
|
|||||||
.then((e) => {
|
.then((e) => {
|
||||||
return { path: x, file: e };
|
return { path: x, file: e };
|
||||||
});
|
});
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filter((x) => x.file)
|
.filter((x) => x.file)
|
||||||
@@ -175,7 +224,7 @@ export async function findContentFile(filePath: string) {
|
|||||||
if (validCandidates.length > 1) {
|
if (validCandidates.length > 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No content path given and more then one candidate found: " +
|
"No content path given and more then one candidate found: " +
|
||||||
validCandidates.join(", "),
|
validCandidates.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (validCandidates.length < 1) {
|
if (validCandidates.length < 1) {
|
||||||
@@ -185,7 +234,7 @@ export async function findContentFile(filePath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function inferContentTypeFromFilePath(
|
export function inferContentTypeFromFilePath(
|
||||||
contentPath: string,
|
contentPath: string
|
||||||
): "python3" | "deno" | "go" | "bash" {
|
): "python3" | "deno" | "go" | "bash" {
|
||||||
let language = contentPath.substring(contentPath.lastIndexOf("."));
|
let language = contentPath.substring(contentPath.lastIndexOf("."));
|
||||||
if (language == ".ts") language = "deno";
|
if (language == ".ts") language = "deno";
|
||||||
@@ -193,7 +242,9 @@ export function inferContentTypeFromFilePath(
|
|||||||
if (language == ".sh") language = "bash";
|
if (language == ".sh") language = "bash";
|
||||||
if (language == ".go") language = "go";
|
if (language == ".go") language = "go";
|
||||||
if (
|
if (
|
||||||
language != "python3" && language != "deno" && language != "go" &&
|
language != "python3" &&
|
||||||
|
language != "deno" &&
|
||||||
|
language != "go" &&
|
||||||
language != "bash"
|
language != "bash"
|
||||||
) {
|
) {
|
||||||
throw new Error("Invalid language: " + language);
|
throw new Error("Invalid language: " + language);
|
||||||
@@ -202,18 +253,20 @@ export function inferContentTypeFromFilePath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function pushScript(
|
export async function pushScript(
|
||||||
filePath: string,
|
filePath: string | undefined,
|
||||||
contentPath: string,
|
contentPath: string,
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string
|
||||||
) {
|
) {
|
||||||
const data = decoverto.type(ScriptFile).rawToInstance(
|
const data = filePath
|
||||||
await Deno.readTextFile(filePath),
|
? decoverto
|
||||||
);
|
.type(ScriptFile)
|
||||||
|
.rawToInstance(await Deno.readTextFile(filePath))
|
||||||
|
: undefined;
|
||||||
const content = await Deno.readTextFile(contentPath);
|
const content = await Deno.readTextFile(contentPath);
|
||||||
|
|
||||||
const language = inferContentTypeFromFilePath(contentPath);
|
const language = inferContentTypeFromFilePath(contentPath);
|
||||||
let parent_hash = data.parent_hash;
|
let parent_hash = data?.parent_hash;
|
||||||
if (!parent_hash) {
|
if (!parent_hash) {
|
||||||
try {
|
try {
|
||||||
parent_hash = (
|
parent_hash = (
|
||||||
@@ -232,15 +285,15 @@ export async function pushScript(
|
|||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
summary: data.summary,
|
summary: data?.summary ?? "",
|
||||||
content: content,
|
content: content,
|
||||||
description: data.description,
|
description: data?.description ?? "",
|
||||||
language: language,
|
language: language,
|
||||||
is_template: data.is_template,
|
is_template: data?.is_template,
|
||||||
kind: data.kind,
|
kind: data?.kind,
|
||||||
lock: data.lock,
|
lock: data?.lock,
|
||||||
parent_hash: parent_hash,
|
parent_hash: parent_hash,
|
||||||
schema: data.schema,
|
schema: data?.schema,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -270,14 +323,7 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
|||||||
.header(["path", "summary", "language", "created by"])
|
.header(["path", "summary", "language", "created by"])
|
||||||
.padding(2)
|
.padding(2)
|
||||||
.border(true)
|
.border(true)
|
||||||
.body(
|
.body(total.map((x) => [x.path, x.summary, x.language, x.created_by]))
|
||||||
total.map((x) => [
|
|
||||||
x.path,
|
|
||||||
x.summary,
|
|
||||||
x.language,
|
|
||||||
x.created_by,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.render();
|
.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,14 +334,15 @@ export async function resolve(input: string): Promise<Record<string, any>> {
|
|||||||
|
|
||||||
if (input == "@-") {
|
if (input == "@-") {
|
||||||
input = new TextDecoder().decode(await readAll(Deno.stdin));
|
input = new TextDecoder().decode(await readAll(Deno.stdin));
|
||||||
} if (input[0] == "@") {
|
}
|
||||||
|
if (input[0] == "@") {
|
||||||
input = await Deno.readTextFile(input.substring(1));
|
input = await Deno.readTextFile(input.substring(1));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(input);
|
return JSON.parse(input);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Impossible to parse input as JSON", input)
|
console.error("Impossible to parse input as JSON", input);
|
||||||
throw e
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,12 +351,11 @@ async function run(
|
|||||||
data?: string;
|
data?: string;
|
||||||
silent: boolean;
|
silent: boolean;
|
||||||
},
|
},
|
||||||
path: string,
|
path: string
|
||||||
) {
|
) {
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
|
|
||||||
const input = opts.data ? await resolve(opts.data) : {};
|
const input = opts.data ? await resolve(opts.data) : {};
|
||||||
const id = await JobService.runScriptByPath({
|
const id = await JobService.runScriptByPath({
|
||||||
workspace: workspace.workspaceId,
|
workspace: workspace.workspaceId,
|
||||||
@@ -323,12 +369,13 @@ async function run(
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const result = (
|
const result =
|
||||||
await JobService.getCompletedJob({
|
(
|
||||||
workspace: workspace.workspaceId,
|
await JobService.getCompletedJob({
|
||||||
id,
|
workspace: workspace.workspaceId,
|
||||||
})
|
id,
|
||||||
).result ?? {};
|
})
|
||||||
|
).result ?? {};
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -343,9 +390,9 @@ export async function track_job(workspace: string, id: string) {
|
|||||||
const result = await JobService.getCompletedJob({ workspace, id });
|
const result = await JobService.getCompletedJob({ workspace, id });
|
||||||
|
|
||||||
console.log(result.logs);
|
console.log(result.logs);
|
||||||
console.log()
|
console.log();
|
||||||
console.log(colors.bold.underline.green("Job Completed"));
|
console.log(colors.bold.underline.green("Job Completed"));
|
||||||
console.log()
|
console.log();
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
@@ -396,7 +443,7 @@ export async function track_job(workspace: string, id: string) {
|
|||||||
if (running && updates.running === false) {
|
if (running && updates.running === false) {
|
||||||
running = false;
|
running = false;
|
||||||
console.log(
|
console.log(
|
||||||
colors.yellow("Job suspended. Waiting for it to continue..."),
|
colors.yellow("Job suspended. Waiting for it to continue...")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,15 +454,13 @@ export async function track_job(workspace: string, id: string) {
|
|||||||
if ((final_job.logs?.length ?? -1) > logOffset) {
|
if ((final_job.logs?.length ?? -1) > logOffset) {
|
||||||
console.log(final_job.logs!.substring(logOffset));
|
console.log(final_job.logs!.substring(logOffset));
|
||||||
}
|
}
|
||||||
console.log("\n")
|
console.log("\n");
|
||||||
if (final_job.success) {
|
if (final_job.success) {
|
||||||
console.log(colors.bold.underline.green("Job Completed"));
|
console.log(colors.bold.underline.green("Job Completed"));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log(colors.bold.underline.red("Job Completed"));
|
console.log(colors.bold.underline.red("Job Completed"));
|
||||||
}
|
}
|
||||||
console.log()
|
console.log();
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
console.log("Job appears to have completed, but no data can be retrieved");
|
console.log("Job appears to have completed, but no data can be retrieved");
|
||||||
}
|
}
|
||||||
@@ -440,22 +485,22 @@ const command = new Command()
|
|||||||
.action(list as any)
|
.action(list as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"push a local script spec. This overrides any remote versions.",
|
"push a local script spec. This overrides any remote versions. Can use a script file (.ts, .js, .py, .sh) or a script spec file (.json). "
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string> [content_path:string]")
|
.arguments("<file_path:file>")
|
||||||
.action(push as any)
|
.action(push as any)
|
||||||
.command("show", "show a scripts content")
|
.command("show", "show a scripts content")
|
||||||
.arguments("<path:string>")
|
.arguments("<path:file>")
|
||||||
.action(show as any)
|
.action(show as any)
|
||||||
.command("run", "run a script by path")
|
.command("run", "run a script by path")
|
||||||
.arguments("<path:string>")
|
.arguments("<path:file>")
|
||||||
.option(
|
.option(
|
||||||
"-d --data <data:string>",
|
"-d --data <data:file>",
|
||||||
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-.",
|
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-s --silent",
|
"-s --silent",
|
||||||
"Do not ouput anything other then the final output. Useful for scripting.",
|
"Do not ouput anything other then the final output. Useful for scripting."
|
||||||
)
|
)
|
||||||
.action(run as any);
|
.action(run as any);
|
||||||
|
|
||||||
|
|||||||
592
cli/sync.ts
592
cli/sync.ts
@@ -13,7 +13,7 @@ import {
|
|||||||
ResourceService,
|
ResourceService,
|
||||||
VariableService,
|
VariableService,
|
||||||
AppService,
|
AppService,
|
||||||
FlowService
|
FlowService,
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import {
|
import {
|
||||||
Difference,
|
Difference,
|
||||||
@@ -25,16 +25,14 @@ import {
|
|||||||
import { downloadZip } from "./pull.ts";
|
import { downloadZip } from "./pull.ts";
|
||||||
import { FolderFile } from "./folder.ts";
|
import { FolderFile } from "./folder.ts";
|
||||||
import { ResourceTypeFile } from "./resource-type.ts";
|
import { ResourceTypeFile } from "./resource-type.ts";
|
||||||
import {
|
import { handleScriptMetadata, ScriptFile } from "./script.ts";
|
||||||
handleScriptMetadata,
|
|
||||||
ScriptFile,
|
|
||||||
} from "./script.ts";
|
|
||||||
import { ResourceFile } from "./resource.ts";
|
import { ResourceFile } from "./resource.ts";
|
||||||
import { FlowFile } from "./flow.ts";
|
import { FlowFile } from "./flow.ts";
|
||||||
import { VariableFile } from "./variable.ts";
|
import { VariableFile } from "./variable.ts";
|
||||||
import { handleFile } from "./script.ts";
|
import { handleFile } from "./script.ts";
|
||||||
import { equal } from "https://deno.land/x/equal/mod.ts";
|
import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts";
|
||||||
import { diffCharacters } from "https://deno.land/x/diff/mod.ts";
|
import * as Diff from "npm:diff";
|
||||||
|
|
||||||
type DynFSElement = {
|
type DynFSElement = {
|
||||||
isDirectory: boolean;
|
isDirectory: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -44,10 +42,7 @@ type DynFSElement = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function FSFSElement(p: string): Promise<DynFSElement> {
|
async function FSFSElement(p: string): Promise<DynFSElement> {
|
||||||
function _internal_element(
|
function _internal_element(localP: string, isDir: boolean): DynFSElement {
|
||||||
localP: string,
|
|
||||||
isDir: boolean,
|
|
||||||
): DynFSElement {
|
|
||||||
return {
|
return {
|
||||||
isDirectory: isDir,
|
isDirectory: isDir,
|
||||||
path: localP.substring(p.length + 1),
|
path: localP.substring(p.length + 1),
|
||||||
@@ -113,16 +108,14 @@ function ZipFSElement(zip: JSZip): DynFSElement {
|
|||||||
|
|
||||||
async function* readDirRecursiveWithIgnore(
|
async function* readDirRecursiveWithIgnore(
|
||||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||||
root: DynFSElement,
|
root: DynFSElement
|
||||||
): AsyncGenerator<
|
): AsyncGenerator<{
|
||||||
{
|
path: string;
|
||||||
path: string;
|
ignored: boolean;
|
||||||
ignored: boolean;
|
isDirectory: boolean;
|
||||||
isDirectory: boolean;
|
getContentBytes(): Promise<Uint8Array>;
|
||||||
getContentBytes(): Promise<Uint8Array>;
|
getContentText(): Promise<string>;
|
||||||
getContentText(): Promise<string>;
|
}> {
|
||||||
}
|
|
||||||
> {
|
|
||||||
const stack: {
|
const stack: {
|
||||||
path: string;
|
path: string;
|
||||||
isDirectory: boolean;
|
isDirectory: boolean;
|
||||||
@@ -130,18 +123,20 @@ async function* readDirRecursiveWithIgnore(
|
|||||||
c(): AsyncIterable<DynFSElement>;
|
c(): AsyncIterable<DynFSElement>;
|
||||||
getContentBytes(): Promise<Uint8Array>;
|
getContentBytes(): Promise<Uint8Array>;
|
||||||
getContentText(): Promise<string>;
|
getContentText(): Promise<string>;
|
||||||
}[] = [{
|
}[] = [
|
||||||
path: root.path,
|
{
|
||||||
ignored: ignore(root.path, root.isDirectory),
|
path: root.path,
|
||||||
isDirectory: root.isDirectory,
|
ignored: ignore(root.path, root.isDirectory),
|
||||||
c: root.getChildren,
|
isDirectory: root.isDirectory,
|
||||||
getContentBytes(): Promise<Uint8Array> {
|
c: root.getChildren,
|
||||||
throw undefined;
|
getContentBytes(): Promise<Uint8Array> {
|
||||||
|
throw undefined;
|
||||||
|
},
|
||||||
|
getContentText(): Promise<string> {
|
||||||
|
throw undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getContentText(): Promise<string> {
|
];
|
||||||
throw undefined;
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const e = stack.pop()!;
|
const e = stack.pop()!;
|
||||||
@@ -161,17 +156,17 @@ async function* readDirRecursiveWithIgnore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Added = { name: "added"; path: string; content: string };
|
type Added = { name: "added"; path: string; content: string };
|
||||||
type Deleted = { name: "deleted"; path: string; };
|
type Deleted = { name: "deleted"; path: string };
|
||||||
type Edit = { name: "edited"; path: string; before: string; after: string; };
|
type Edit = { name: "edited"; path: string; before: string; after: string };
|
||||||
|
|
||||||
type Change = Added | Deleted | Edit;
|
type Change = Added | Deleted | Edit;
|
||||||
|
|
||||||
async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirectory: boolean) => boolean): Promise<{ [key: string]: string }> {
|
async function elementsToMap(
|
||||||
|
els: DynFSElement,
|
||||||
|
ignore: (path: string, isDirectory: boolean) => boolean
|
||||||
|
): Promise<{ [key: string]: string }> {
|
||||||
const map: { [key: string]: string } = {};
|
const map: { [key: string]: string } = {};
|
||||||
for await (const entry of readDirRecursiveWithIgnore(
|
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
|
||||||
ignore,
|
|
||||||
els,
|
|
||||||
)) {
|
|
||||||
if (entry.isDirectory || entry.ignored) continue;
|
if (entry.isDirectory || entry.ignored) continue;
|
||||||
const content = await entry.getContentText();
|
const content = await entry.getContentText();
|
||||||
map[entry.path] = content;
|
map[entry.path] = content;
|
||||||
@@ -179,20 +174,26 @@ async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirecto
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
async function compareDynFSElement(
|
async function compareDynFSElement(
|
||||||
els1: DynFSElement, els2: DynFSElement,
|
els1: DynFSElement,
|
||||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
els2: DynFSElement | undefined,
|
||||||
raw: boolean
|
ignore: (path: string, isDirectory: boolean) => boolean
|
||||||
): Promise<Change[]> {
|
): Promise<Change[]> {
|
||||||
|
const [m1, m2] = els2
|
||||||
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
|
? await Promise.all([
|
||||||
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]);
|
elementsToMap(els1, ignore),
|
||||||
|
elementsToMap(els2, ignore),
|
||||||
|
])
|
||||||
|
: [await elementsToMap(els1, ignore), {}];
|
||||||
|
|
||||||
const changes: Change[] = [];
|
const changes: Change[] = [];
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(m1)) {
|
for (const [k, v] of Object.entries(m1)) {
|
||||||
if (m2[k] === undefined) {
|
if (m2[k] === undefined) {
|
||||||
changes.push({ name: "added", path: k, content: v });
|
changes.push({ name: "added", path: k, content: v });
|
||||||
} else if (m2[k] != v && (!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))) {
|
} else if (
|
||||||
|
m2[k] != v &&
|
||||||
|
(!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))
|
||||||
|
) {
|
||||||
// await Deno.writeTextFile("/tmp/k", m2[k])
|
// await Deno.writeTextFile("/tmp/k", m2[k])
|
||||||
// await Deno.writeTextFile("/tmp/v", v)
|
// await Deno.writeTextFile("/tmp/v", v)
|
||||||
// console.log(k)
|
// console.log(k)
|
||||||
@@ -208,54 +209,59 @@ async function compareDynFSElement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return changes
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNotWmillFile = (p: string, isDirectory: boolean) => {
|
const isNotWmillFile = (p: string, isDirectory: boolean) => {
|
||||||
if (p.endsWith("/")) {
|
if (p.endsWith("/")) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
|
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const typ = getTypeStrFromPath(p)
|
const typ = getTypeStrFromPath(p);
|
||||||
if (typ == 'resource-type') {
|
if (typ == "resource-type") {
|
||||||
return p.includes('/')
|
return p.includes("/");
|
||||||
} else {
|
} else {
|
||||||
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
|
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const isWhitelisted = (p: string) => {
|
const isWhitelisted = (p: string) => {
|
||||||
return p == "./" || p == "" || p == "u" || p == "f" || p == "g"
|
return p == "./" || p == "" || p == "u" || p == "f" || p == "g";
|
||||||
}
|
};
|
||||||
async function ignoreF() {
|
async function ignoreF() {
|
||||||
try {
|
try {
|
||||||
const ignore: {
|
const ignore: {
|
||||||
accepts(file: string): boolean;
|
accepts(file: string): boolean;
|
||||||
denies(file: string): boolean;
|
denies(file: string): boolean;
|
||||||
} = gitignore_parser.compile(
|
} = gitignore_parser.compile(await Deno.readTextFile(".wmillignore"));
|
||||||
await Deno.readTextFile(".wmillignore"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (p: string, isDirectory: boolean) => {
|
return (p: string, isDirectory: boolean) => {
|
||||||
return !isWhitelisted(p) && (isNotWmillFile(p, isDirectory) || ignore.denies(p));
|
return (
|
||||||
}
|
!isWhitelisted(p) &&
|
||||||
|
(isNotWmillFile(p, isDirectory) || ignore.denies(p))
|
||||||
|
);
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return (p: string, isDirectory: boolean) => !isWhitelisted(p) && isNotWmillFile(p, isDirectory)
|
return (p: string, isDirectory: boolean) =>
|
||||||
|
!isWhitelisted(p) && isNotWmillFile(p, isDirectory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pull(
|
async function pull(
|
||||||
opts: GlobalOptions & { raw: boolean; yes: boolean, failConflicts: boolean },
|
opts: GlobalOptions & {
|
||||||
|
raw: boolean;
|
||||||
|
yes: boolean;
|
||||||
|
failConflicts: boolean;
|
||||||
|
plainSecrets?: boolean;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
await ensureDir(path.join(Deno.cwd(), ".wmill"));
|
await ensureDir(path.join(Deno.cwd(), ".wmill"));
|
||||||
}
|
}
|
||||||
@@ -263,74 +269,92 @@ async function pull(
|
|||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
|
console.log(
|
||||||
console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)"));
|
colors.gray(
|
||||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
"Computing the files to update locally to match remote (taking .wmillignore into account)"
|
||||||
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
|
)
|
||||||
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
|
);
|
||||||
|
const remote = ZipFSElement(
|
||||||
|
(await downloadZip(workspace, opts.plainSecrets))!
|
||||||
|
);
|
||||||
|
const local = opts.raw
|
||||||
|
? undefined
|
||||||
|
: await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"));
|
||||||
|
const changes = await compareDynFSElement(remote, local, await ignoreF());
|
||||||
|
|
||||||
console.log(`remote -> local: ${changes.length} changes to apply`);
|
console.log(`remote -> local: ${changes.length} changes to apply`);
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
|
prettyChanges(changes);
|
||||||
prettyChanges(changes)
|
|
||||||
if (
|
if (
|
||||||
!opts.yes && !opts.raw && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
|
!opts.yes &&
|
||||||
|
!opts.raw &&
|
||||||
|
!(await Confirm.prompt({
|
||||||
|
message: `Do you want to apply these ${changes.length} changes?`,
|
||||||
|
default: true,
|
||||||
|
}))
|
||||||
) {
|
) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const conflicts = []
|
const conflicts = [];
|
||||||
console.log(colors.gray(`Applying changes to files ...`));
|
console.log(colors.gray(`Applying changes to files ...`));
|
||||||
for await (const change of changes) {
|
for await (const change of changes) {
|
||||||
const target = path.join(Deno.cwd(), change.path);
|
const target = path.join(Deno.cwd(), change.path);
|
||||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
|
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||||
if (change.name === "edited") {
|
if (change.name === "edited") {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentLocal = await Deno.readTextFile(target)
|
const currentLocal = await Deno.readTextFile(target);
|
||||||
if (currentLocal !== change.before) {
|
if (currentLocal !== change.before && currentLocal !== change.after) {
|
||||||
console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`))
|
console.log(
|
||||||
|
colors.red(
|
||||||
|
`Conflict detected on ${change.path}\nBoth local and remote have been modified.`
|
||||||
|
)
|
||||||
|
);
|
||||||
if (opts.failConflicts) {
|
if (opts.failConflicts) {
|
||||||
conflicts.push({ local: currentLocal, change, path: change.path })
|
conflicts.push({
|
||||||
|
local: currentLocal,
|
||||||
|
change,
|
||||||
|
path: change.path,
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
} else if (opts.yes) {
|
} else if (opts.yes) {
|
||||||
console.log(colors.red(`Override local version with remote since --yes was passed and no --fail-conflicts.`))
|
console.log(
|
||||||
}
|
colors.red(
|
||||||
else {
|
`Override local version with remote since --yes was passed and no --fail-conflicts.`
|
||||||
showConflict(change.path, currentLocal, change.after)
|
)
|
||||||
if (await Confirm.prompt("Preserve local (push to change remote and avoid seeing this again)?")) {
|
);
|
||||||
|
} else {
|
||||||
|
showConflict(change.path, currentLocal, change.after);
|
||||||
|
if (
|
||||||
|
await Confirm.prompt(
|
||||||
|
"Preserve local (push to change remote and avoid seeing this again)?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {}
|
||||||
if (change.path.endsWith(".json")) {
|
if (!change.path.endsWith(".json")) {
|
||||||
const diffs =
|
console.log(`Editing script content of ${change.path}`);
|
||||||
microdiff(
|
|
||||||
JSON.parse(change.before),
|
|
||||||
JSON.parse(change.after),
|
|
||||||
{ cyclesFix: false },
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`)
|
|
||||||
await applyDiff(
|
|
||||||
diffs,
|
|
||||||
target,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`Editing script ${change.path}`)
|
console.log(
|
||||||
await Deno.writeTextFile(target, change.after);
|
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
await Deno.writeTextFile(target, change.after);
|
||||||
|
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
await ensureDir(path.dirname(stateTarget))
|
await ensureDir(path.dirname(stateTarget));
|
||||||
await Deno.copyFile(target, stateTarget);
|
await Deno.copyFile(target, stateTarget);
|
||||||
}
|
}
|
||||||
} else if (change.name === "added") {
|
} else if (change.name === "added") {
|
||||||
await ensureDir(path.dirname(target))
|
await ensureDir(path.dirname(target));
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
await ensureDir(path.dirname(stateTarget))
|
await ensureDir(path.dirname(stateTarget));
|
||||||
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
|
console.log(
|
||||||
|
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await Deno.writeTextFile(target, change.content);
|
await Deno.writeTextFile(target, change.content);
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
@@ -338,8 +362,10 @@ async function pull(
|
|||||||
}
|
}
|
||||||
} else if (change.name === "deleted") {
|
} else if (change.name === "deleted") {
|
||||||
try {
|
try {
|
||||||
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
|
console.log(
|
||||||
await Deno.remove(target)
|
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||||
|
);
|
||||||
|
await Deno.remove(target);
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
await Deno.remove(stateTarget);
|
await Deno.remove(stateTarget);
|
||||||
}
|
}
|
||||||
@@ -352,238 +378,301 @@ async function pull(
|
|||||||
}
|
}
|
||||||
if (opts.failConflicts) {
|
if (opts.failConflicts) {
|
||||||
if (conflicts.length > 0) {
|
if (conflicts.length > 0) {
|
||||||
console.error(colors.red(`Conflicts were found`))
|
console.error(colors.red(`Conflicts were found`));
|
||||||
console.log("Conflicts:")
|
console.log("Conflicts:");
|
||||||
for (const conflict of conflicts) {
|
for (const conflict of conflicts) {
|
||||||
showConflict(conflict.path, conflict.local, conflict.change.after)
|
showConflict(conflict.path, conflict.local, conflict.change.after);
|
||||||
}
|
}
|
||||||
console.log(colors.red(`Please resolve theses conflicts manually by either:
|
console.log(
|
||||||
|
colors.red(`Please resolve theses conflicts manually by either:
|
||||||
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
|
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
|
||||||
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
|
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
|
||||||
`))
|
`)
|
||||||
Deno.exit(1)
|
);
|
||||||
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(colors.green.underline(`Done! All ${changes.length} changes applied locally.`));
|
console.log(
|
||||||
|
colors.green.underline(
|
||||||
|
`Done! All ${changes.length} changes applied locally.`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function showConflict(path: string, local: string, remote: string) {
|
function showConflict(path: string, local: string, remote: string) {
|
||||||
console.log(colors.yellow(`- ${path}`))
|
console.log(colors.yellow(`- ${path}`));
|
||||||
|
|
||||||
let finalString = "";
|
let finalString = "";
|
||||||
for (const character of diffCharacters(local, remote)) {
|
for (const part of Diff.diffLines(local, remote)) {
|
||||||
if (character.wasRemoved) {
|
if (part.removed) {
|
||||||
// print red if removed without newline
|
// print red if removed without newline
|
||||||
finalString += `\x1b[31m${character.character}\x1b[0m`;
|
finalString += `\x1b[31m${part.value}\x1b[0m`;
|
||||||
} else if (character.wasAdded) {
|
} else if (part.added) {
|
||||||
// print green if added
|
// print green if added
|
||||||
finalString += `\x1b[32m${character.character}\x1b[0m`;
|
finalString += `\x1b[32m${part.value}\x1b[0m`;
|
||||||
} else {
|
} else {
|
||||||
// print white if unchanged
|
// print white if unchanged
|
||||||
finalString += `\x1b[37m${character.character}\x1b[0m`;
|
finalString += `\x1b[37m${part.value}\x1b[0m`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(finalString);
|
console.log(finalString);
|
||||||
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m")
|
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m");
|
||||||
console.log()
|
console.log();
|
||||||
}
|
|
||||||
async function applyDiff(diffs: Difference[], file: string) {
|
|
||||||
ensureDir(path.dirname(file));
|
|
||||||
let json;
|
|
||||||
try {
|
|
||||||
json = JSON.parse(await Deno.readTextFile(file));
|
|
||||||
} catch {
|
|
||||||
json = {};
|
|
||||||
}
|
|
||||||
// TODO: Delegate the below to the object itself
|
|
||||||
// This would work by infering the type of `JSON` (which includes then statically typing it using decoverto) and then
|
|
||||||
// delegating the applying of the diffs to the object via an interface
|
|
||||||
for (const diff of diffs) {
|
|
||||||
if (diff.type === "CREATE") {
|
|
||||||
setValueByPath(json, diff.path, diff.value);
|
|
||||||
} else if (diff.type === "REMOVE") {
|
|
||||||
setValueByPath(json, diff.path, undefined);
|
|
||||||
} else if (diff.type === "CHANGE") {
|
|
||||||
setValueByPath(json, diff.path, diff.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Deno.writeTextFile(file, JSON.stringify(json, undefined, " "), {
|
|
||||||
create: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function prettyChanges(changes: Change[]) {
|
function prettyChanges(changes: Change[]) {
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.name === "added") {
|
if (change.name === "added") {
|
||||||
console.log(colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path));
|
console.log(
|
||||||
|
colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||||
|
);
|
||||||
} else if (change.name === "deleted") {
|
} else if (change.name === "deleted") {
|
||||||
console.log(colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path));
|
console.log(
|
||||||
|
colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||||
|
);
|
||||||
} else if (change.name === "edited") {
|
} else if (change.name === "edited") {
|
||||||
console.log(colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path));
|
console.log(
|
||||||
}
|
colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function prettyDiff(diffs: Difference[]) {
|
|
||||||
for (const diff of diffs) {
|
|
||||||
let pathString = "";
|
|
||||||
for (const pathSegment of diff.path) {
|
|
||||||
if (typeof pathSegment === "string") {
|
|
||||||
pathString += ".";
|
|
||||||
pathString += pathSegment;
|
|
||||||
} else {
|
|
||||||
pathString += "[";
|
|
||||||
pathString += pathSegment;
|
|
||||||
pathString += "]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (diff.type === "REMOVE" || diff.type === "CHANGE") {
|
|
||||||
console.log(colors.red("- " + pathString + " = " + diff.oldValue));
|
|
||||||
}
|
|
||||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
|
||||||
console.log(colors.green("+ " + pathString + " = " + diff.value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function prettyDiff(diffs: Difference[]) {
|
||||||
|
// for (const diff of diffs) {
|
||||||
|
// let pathString = "";
|
||||||
|
// for (const pathSegment of diff.path) {
|
||||||
|
// if (typeof pathSegment === "string") {
|
||||||
|
// pathString += ".";
|
||||||
|
// pathString += pathSegment;
|
||||||
|
// } else {
|
||||||
|
// pathString += "[";
|
||||||
|
// pathString += pathSegment;
|
||||||
|
// pathString += "]";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (diff.type === "REMOVE" || diff.type === "CHANGE") {
|
||||||
|
// console.log(colors.red("- " + pathString + " = " + diff.oldValue));
|
||||||
|
// }
|
||||||
|
// if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||||
|
// console.log(colors.green("+ " + pathString + " = " + diff.value));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
function removeSuffix(str: string, suffix: string) {
|
function removeSuffix(str: string, suffix: string) {
|
||||||
return str.slice(0, str.length - suffix.length);
|
return str.slice(0, str.length - suffix.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) {
|
async function push(
|
||||||
|
opts: GlobalOptions & {
|
||||||
|
raw: boolean;
|
||||||
|
yes: boolean;
|
||||||
|
skipPull: boolean;
|
||||||
|
failConflicts: boolean;
|
||||||
|
plainSecrets?: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
if (!opts.skipPull) {
|
if (!opts.skipPull) {
|
||||||
console.log(colors.gray("You need to be up-to-date before pushing, pulling first."))
|
console.log(
|
||||||
await pull(opts)
|
colors.gray("You need to be up-to-date before pushing, pulling first.")
|
||||||
console.log(colors.green("Pull done, now pushing."))
|
);
|
||||||
console.log()
|
await pull(opts);
|
||||||
|
console.log(colors.green("Pull done, now pushing."));
|
||||||
|
console.log();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
|
console.log(
|
||||||
console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)"));
|
colors.gray(
|
||||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
"Computing the files to update on the remote to match local (taking .wmillignore into account)"
|
||||||
const local = await FSFSElement(path.join(Deno.cwd(), ""))
|
)
|
||||||
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
|
);
|
||||||
|
const remote = opts.raw
|
||||||
|
? undefined
|
||||||
|
: ZipFSElement((await downloadZip(workspace, opts.plainSecrets))!);
|
||||||
|
const local = await FSFSElement(path.join(Deno.cwd(), ""));
|
||||||
|
const changes = await compareDynFSElement(local, remote, await ignoreF());
|
||||||
|
|
||||||
console.log(`remote <- local: ${changes.length} changes to apply`);
|
console.log(`remote <- local: ${changes.length} changes to apply`);
|
||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
|
prettyChanges(changes);
|
||||||
prettyChanges(changes)
|
|
||||||
if (
|
if (
|
||||||
!opts.yes && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
|
!opts.yes &&
|
||||||
|
!(await Confirm.prompt({
|
||||||
|
message: `Do you want to apply these ${changes.length} changes?`,
|
||||||
|
default: true,
|
||||||
|
}))
|
||||||
) {
|
) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
console.log(colors.gray(`Applying changes to files ...`));
|
console.log(colors.gray(`Applying changes to files ...`));
|
||||||
const alreadySynced: string[] = []
|
const alreadySynced: string[] = [];
|
||||||
for await (const change of changes) {
|
for await (const change of changes) {
|
||||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
|
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||||
|
let stateExists = true;
|
||||||
|
try {
|
||||||
|
await Deno.stat(stateTarget);
|
||||||
|
} catch {
|
||||||
|
stateExists = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (change.name === "edited") {
|
if (change.name === "edited") {
|
||||||
if (await handleScriptMetadata(change.path, workspace.workspaceId, alreadySynced)) {
|
if (
|
||||||
if (!opts.raw) {
|
await handleScriptMetadata(
|
||||||
|
change.path,
|
||||||
|
workspace.workspaceId,
|
||||||
|
alreadySynced
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!opts.raw && stateExists) {
|
||||||
await Deno.writeTextFile(stateTarget, change.after);
|
await Deno.writeTextFile(stateTarget, change.after);
|
||||||
}
|
}
|
||||||
continue
|
continue;
|
||||||
} else if (await handleFile(change.path, change.after, workspace.workspaceId, alreadySynced)) {
|
} else if (
|
||||||
if (!opts.raw) {
|
await handleFile(
|
||||||
|
change.path,
|
||||||
|
change.after,
|
||||||
|
workspace.workspaceId,
|
||||||
|
alreadySynced
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!opts.raw && stateExists) {
|
||||||
await Deno.writeTextFile(stateTarget, change.after);
|
await Deno.writeTextFile(stateTarget, change.after);
|
||||||
}
|
}
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
if (!opts.raw) {
|
if (!opts.raw) {
|
||||||
await ensureDir(path.dirname(stateTarget))
|
await ensureDir(path.dirname(stateTarget));
|
||||||
console.log(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`)
|
console.log(
|
||||||
|
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const obj = inferTypeFromPath(change.path, JSON.parse(change.after))
|
const obj = inferTypeFromPath(change.path, JSON.parse(change.after));
|
||||||
|
|
||||||
const diff = microdiff(inferTypeFromPath(change.path, JSON.parse(change.before)), obj, { cyclesFix: false });
|
const diff = microdiff(
|
||||||
|
inferTypeFromPath(change.path, JSON.parse(change.before)),
|
||||||
|
obj,
|
||||||
|
{ cyclesFix: false }
|
||||||
|
);
|
||||||
await applyDiff(
|
await applyDiff(
|
||||||
workspace.workspaceId,
|
workspace.workspaceId,
|
||||||
change.path.split(".")[0],
|
change.path.split(".")[0],
|
||||||
obj,
|
obj,
|
||||||
diff,
|
diff,
|
||||||
|
opts.plainSecrets
|
||||||
);
|
);
|
||||||
if (!opts.raw) {
|
if (!opts.raw && stateExists) {
|
||||||
await Deno.writeTextFile(stateTarget, change.after);
|
await Deno.writeTextFile(stateTarget, change.after);
|
||||||
}
|
}
|
||||||
} else if (change.name === "added") {
|
} else if (change.name === "added") {
|
||||||
if (change.path.endsWith(".script.json")) {
|
if (change.path.endsWith(".script.json")) {
|
||||||
continue
|
continue;
|
||||||
} else if (await handleFile(change.path, change.content, workspace.workspaceId, alreadySynced)) {
|
} else if (
|
||||||
continue
|
await handleFile(
|
||||||
|
change.path,
|
||||||
|
change.content,
|
||||||
|
workspace.workspaceId,
|
||||||
|
alreadySynced
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (!opts.raw) {
|
if (!opts.raw && stateExists) {
|
||||||
await ensureDir(path.dirname(stateTarget))
|
await ensureDir(path.dirname(stateTarget));
|
||||||
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
|
console.log(
|
||||||
|
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const obj = inferTypeFromPath(change.path, JSON.parse(change.content))
|
const obj = inferTypeFromPath(change.path, JSON.parse(change.content));
|
||||||
const diff = microdiff({}, obj, { cyclesFix: false });
|
const diff = microdiff({}, obj, { cyclesFix: false });
|
||||||
await applyDiff(
|
await applyDiff(
|
||||||
workspace.workspaceId,
|
workspace.workspaceId,
|
||||||
change.path.split(".")[0],
|
change.path.split(".")[0],
|
||||||
obj,
|
obj,
|
||||||
diff,
|
diff,
|
||||||
|
opts.plainSecrets
|
||||||
);
|
);
|
||||||
if (!opts.raw) {
|
if (!opts.raw && stateExists) {
|
||||||
await Deno.writeTextFile(stateTarget, change.content);
|
await Deno.writeTextFile(stateTarget, change.content);
|
||||||
}
|
}
|
||||||
} else if (change.name === "deleted") {
|
} else if (change.name === "deleted") {
|
||||||
if (!change.path.includes(".json")) {
|
if (!change.path.includes(".json")) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
|
console.log(
|
||||||
const typ = getTypeStrFromPath(change.path)
|
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||||
|
);
|
||||||
|
const typ = getTypeStrFromPath(change.path);
|
||||||
const workspaceId = workspace.workspaceId;
|
const workspaceId = workspace.workspaceId;
|
||||||
switch (typ) {
|
switch (typ) {
|
||||||
case "script": {
|
case "script": {
|
||||||
const script = await ScriptService.getScriptByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".script.json") })
|
const script = await ScriptService.getScriptByPath({
|
||||||
await ScriptService.deleteScriptByHash({ workspace: workspaceId, hash: script.hash })
|
workspace: workspaceId,
|
||||||
|
path: removeSuffix(change.path, ".script.json"),
|
||||||
|
});
|
||||||
|
await ScriptService.deleteScriptByHash({
|
||||||
|
workspace: workspaceId,
|
||||||
|
hash: script.hash,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "folder":
|
case "folder":
|
||||||
await FolderService.deleteFolder({ workspace: workspaceId, name: change.path.split('/')[1] })
|
await FolderService.deleteFolder({
|
||||||
|
workspace: workspaceId,
|
||||||
|
name: change.path.split("/")[1],
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "resource":
|
case "resource":
|
||||||
await ResourceService.deleteResource({ workspace: workspaceId, path: removeSuffix(change.path, ".resource.json") })
|
await ResourceService.deleteResource({
|
||||||
|
workspace: workspaceId,
|
||||||
|
path: removeSuffix(change.path, ".resource.json"),
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "resource-type":
|
case "resource-type":
|
||||||
await ResourceService.deleteResourceType({ workspace: workspaceId, path: removeSuffix(change.path, ".resource-type.json") })
|
await ResourceService.deleteResourceType({
|
||||||
break
|
workspace: workspaceId,
|
||||||
|
path: removeSuffix(change.path, ".resource-type.json"),
|
||||||
|
});
|
||||||
|
break;
|
||||||
case "flow":
|
case "flow":
|
||||||
await FlowService.deleteFlowByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".flow.json") })
|
await FlowService.deleteFlowByPath({
|
||||||
break
|
workspace: workspaceId,
|
||||||
|
path: removeSuffix(change.path, ".flow.json"),
|
||||||
|
});
|
||||||
|
break;
|
||||||
case "app":
|
case "app":
|
||||||
await AppService.deleteApp({ workspace: workspaceId, path: removeSuffix(change.path, ".app.json") })
|
await AppService.deleteApp({
|
||||||
break
|
workspace: workspaceId,
|
||||||
|
path: removeSuffix(change.path, ".app.json"),
|
||||||
|
});
|
||||||
|
break;
|
||||||
case "variable":
|
case "variable":
|
||||||
await VariableService.deleteVariable({ workspace: workspaceId, path: removeSuffix(change.path, ".variable.json") })
|
await VariableService.deleteVariable({
|
||||||
break
|
workspace: workspaceId,
|
||||||
|
path: removeSuffix(change.path, ".variable.json"),
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Deno.remove(stateTarget)
|
await Deno.remove(stateTarget);
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(colors.green.underline(`Done! All ${changes.length} changes pushed to the remote workspace.`));
|
console.log(
|
||||||
|
colors.green.underline(
|
||||||
|
`Done! All ${changes.length} changes pushed to the remote workspace.`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function applyDiff(
|
async function applyDiff(
|
||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
@@ -595,10 +684,11 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
|||||||
| ResourceTypeFile
|
| ResourceTypeFile
|
||||||
| FolderFile,
|
| FolderFile,
|
||||||
diffs: Difference[],
|
diffs: Difference[],
|
||||||
|
plainSecrets?: boolean
|
||||||
) {
|
) {
|
||||||
if (file instanceof ScriptFile) {
|
if (file instanceof ScriptFile) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"This code path should be unreachable - we should never generate diffs for scripts",
|
"This code path should be unreachable - we should never generate diffs for scripts"
|
||||||
);
|
);
|
||||||
} else if (file instanceof FolderFile) {
|
} else if (file instanceof FolderFile) {
|
||||||
const parts = remotePath.split("/");
|
const parts = remotePath.split("/");
|
||||||
@@ -609,14 +699,14 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (diffs.length === 0) {
|
if (diffs.length === 0) {
|
||||||
console.log("No diffs to apply to " + remotePath)
|
console.log("No diffs to apply to " + remotePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await file.pushDiffs(workspace, remotePath, diffs);
|
await file.pushDiffs(workspace, remotePath, diffs, plainSecrets);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failing to apply diffs to " + remotePath)
|
console.error("Failing to apply diffs to " + remotePath);
|
||||||
console.error(e.body)
|
console.error(JSON.stringify(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,20 +714,28 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
|||||||
const command = new Command()
|
const command = new Command()
|
||||||
.command("pull")
|
.command("pull")
|
||||||
.description(
|
.description(
|
||||||
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking.",
|
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking."
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--fail-conflicts",
|
||||||
|
"Error on conflicts (both remote and local have changes on the same item)"
|
||||||
)
|
)
|
||||||
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
|
|
||||||
.option("--yes", "Pull without needing confirmation")
|
.option("--yes", "Pull without needing confirmation")
|
||||||
.option("--raw", "Pull without using state, just overwrite.")
|
.option("--raw", "Pull without using state, just overwrite.")
|
||||||
|
.option("--plain-secrets", "Pull secrets as plain text")
|
||||||
.action(pull as any)
|
.action(pull as any)
|
||||||
.command("push")
|
.command("push")
|
||||||
.description(
|
.description(
|
||||||
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking.",
|
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking."
|
||||||
)
|
)
|
||||||
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
|
.option(
|
||||||
.option("--skip-pull", "Push without pulling first")
|
"--fail-conflicts",
|
||||||
|
"Error on conflicts (both remote and local have changes on the same item)"
|
||||||
|
)
|
||||||
|
.option("--skip-pull", "Push without pulling first (you have pulled prior)")
|
||||||
.option("--yes", "Push without needing confirmation")
|
.option("--yes", "Push without needing confirmation")
|
||||||
.option("--raw", "Push without using state, just overwrite.")
|
.option("--raw", "Push without using state, just overwrite.")
|
||||||
|
.option("--plain-secrets", "Push secrets as plain text")
|
||||||
.action(push as any);
|
.action(push as any);
|
||||||
|
|
||||||
export default command;
|
export default command;
|
||||||
|
|||||||
31
cli/types.ts
31
cli/types.ts
@@ -11,7 +11,11 @@ import { AppFile } from "./apps.ts";
|
|||||||
// TODO: Remove this & replace with a "pull" that lets the object either pull the remote version or return undefined.
|
// TODO: Remove this & replace with a "pull" that lets the object either pull the remote version or return undefined.
|
||||||
// Then combine those with diffing, which then gives the new push impl
|
// Then combine those with diffing, which then gives the new push impl
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
push(workspace: string, remotePath: string): Promise<void>;
|
push(
|
||||||
|
workspace: string,
|
||||||
|
remotePath: string,
|
||||||
|
plainSecrets?: boolean
|
||||||
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PushDiffs {
|
export interface PushDiffs {
|
||||||
@@ -19,6 +23,7 @@ export interface PushDiffs {
|
|||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[],
|
diffs: Difference[],
|
||||||
|
plainSecrets?: boolean
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +51,7 @@ export type Difference = DifferenceCreate | DifferenceRemove | DifferenceChange;
|
|||||||
export function setValueByPath(
|
export function setValueByPath(
|
||||||
obj: any,
|
obj: any,
|
||||||
path: (string | number)[],
|
path: (string | number)[],
|
||||||
value: any,
|
value: any
|
||||||
) {
|
) {
|
||||||
let i;
|
let i;
|
||||||
let lastObj = undefined;
|
let lastObj = undefined;
|
||||||
@@ -84,7 +89,7 @@ export type GlobalOptions = {
|
|||||||
|
|
||||||
export function inferTypeFromPath(
|
export function inferTypeFromPath(
|
||||||
p: string,
|
p: string,
|
||||||
obj: any,
|
obj: any
|
||||||
):
|
):
|
||||||
| ScriptFile
|
| ScriptFile
|
||||||
| VariableFile
|
| VariableFile
|
||||||
@@ -115,7 +120,7 @@ export function inferTypeFromPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeStrFromPath(
|
export function getTypeStrFromPath(
|
||||||
p: string,
|
p: string
|
||||||
):
|
):
|
||||||
| "script"
|
| "script"
|
||||||
| "variable"
|
| "variable"
|
||||||
@@ -125,8 +130,13 @@ export function getTypeStrFromPath(
|
|||||||
| "folder"
|
| "folder"
|
||||||
| "app" {
|
| "app" {
|
||||||
const parsed = path.parse(p);
|
const parsed = path.parse(p);
|
||||||
if (parsed.ext == ".go" || parsed.ext == ".ts" || parsed.ext == ".sh" || parsed.ext == ".py") {
|
if (
|
||||||
return 'script'
|
parsed.ext == ".go" ||
|
||||||
|
parsed.ext == ".ts" ||
|
||||||
|
parsed.ext == ".sh" ||
|
||||||
|
parsed.ext == ".py"
|
||||||
|
) {
|
||||||
|
return "script";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.name === "folder.meta") {
|
if (parsed.name === "folder.meta") {
|
||||||
@@ -135,9 +145,12 @@ export function getTypeStrFromPath(
|
|||||||
|
|
||||||
const typeEnding = parsed.name.split(".").at(-1);
|
const typeEnding = parsed.name.split(".").at(-1);
|
||||||
if (
|
if (
|
||||||
typeEnding === "script" || typeEnding === "variable" ||
|
typeEnding === "script" ||
|
||||||
typeEnding === "flow" || typeEnding === "resource" ||
|
typeEnding === "variable" ||
|
||||||
typeEnding === "resource-type" || typeEnding === "app"
|
typeEnding === "flow" ||
|
||||||
|
typeEnding === "resource" ||
|
||||||
|
typeEnding === "resource-type" ||
|
||||||
|
typeEnding === "app"
|
||||||
) {
|
) {
|
||||||
return typeEnding;
|
return typeEnding;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
colors,
|
colors,
|
||||||
Command,
|
Command,
|
||||||
EditVariable,
|
EditVariable,
|
||||||
ListableVariable,
|
|
||||||
microdiff,
|
microdiff,
|
||||||
Table,
|
Table,
|
||||||
VariableService,
|
VariableService,
|
||||||
@@ -36,7 +35,7 @@ async function list(opts: GlobalOptions) {
|
|||||||
x.is_secret ? "true" : "false",
|
x.is_secret ? "true" : "false",
|
||||||
x.account ?? "-",
|
x.account ?? "-",
|
||||||
x.value ?? "-",
|
x.value ?? "-",
|
||||||
]),
|
])
|
||||||
)
|
)
|
||||||
.render();
|
.render();
|
||||||
}
|
}
|
||||||
@@ -63,24 +62,31 @@ export class VariableFile implements Resource, PushDiffs {
|
|||||||
workspace: string,
|
workspace: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
diffs: Difference[],
|
diffs: Difference[],
|
||||||
|
plainSecrets?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
|
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.bold.yellow(
|
colors.bold.yellow(
|
||||||
`Applying ${diffs.length} diffs to existing variable...`,
|
`Applying ${diffs.length} diffs to existing variable... ${remotePath}`
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const changeset: EditVariable = {};
|
const changeset: EditVariable = {};
|
||||||
for (const diff of diffs) {
|
for (const diff of diffs) {
|
||||||
if (
|
if (
|
||||||
diff.type !== "REMOVE" &&
|
diff.type !== "REMOVE" &&
|
||||||
(
|
(diff.path.length !== 1 ||
|
||||||
diff.path.length !== 1 ||
|
![
|
||||||
!["path", "value", "is_secret", "description"].includes(
|
"path",
|
||||||
diff.path[0] as string,
|
"value",
|
||||||
)
|
"is_secret",
|
||||||
)
|
"description",
|
||||||
|
"account",
|
||||||
|
"is_oauth",
|
||||||
|
].includes(diff.path[0] as string))
|
||||||
) {
|
) {
|
||||||
|
console.log(
|
||||||
|
colors.red("Invalid variable diff with path " + diff.path)
|
||||||
|
);
|
||||||
throw new Error("Invalid variable diff with path " + diff.path);
|
throw new Error("Invalid variable diff with path " + diff.path);
|
||||||
}
|
}
|
||||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||||
@@ -90,25 +96,24 @@ export class VariableFile implements Resource, PushDiffs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanges = Object.values(changeset).some((v) =>
|
const hasChanges = Object.values(changeset).some(
|
||||||
v !== null && typeof v !== "undefined"
|
(v) => v !== null && typeof v !== "undefined"
|
||||||
);
|
);
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await VariableService.updateVariable({
|
await VariableService.updateVariable({
|
||||||
workspace,
|
workspace,
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
alreadyEncrypted: true,
|
alreadyEncrypted: !plainSecrets,
|
||||||
requestBody: changeset,
|
requestBody: changeset,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(changeset);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(colors.yellow.bold("Creating new variable..."));
|
console.log(colors.yellow.bold("Creating new variable..."));
|
||||||
await VariableService.createVariable({
|
await VariableService.createVariable({
|
||||||
workspace,
|
workspace,
|
||||||
alreadyEncrypted: true,
|
alreadyEncrypted: !plainSecrets,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
path: remotePath,
|
path: remotePath,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
@@ -120,29 +125,29 @@ export class VariableFile implements Resource, PushDiffs {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async push(workspace: string, remotePath: string): Promise<void> {
|
async push(
|
||||||
let existing: ListableVariable | undefined;
|
workspace: string,
|
||||||
try {
|
remotePath: string,
|
||||||
existing = await VariableService.getVariable({
|
plainSecrets?: boolean
|
||||||
workspace: workspace,
|
): Promise<void> {
|
||||||
path: remotePath,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
existing = undefined;
|
|
||||||
}
|
|
||||||
await this.pushDiffs(
|
await this.pushDiffs(
|
||||||
workspace,
|
workspace,
|
||||||
remotePath,
|
remotePath,
|
||||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
microdiff({}, this, { cyclesFix: false }),
|
||||||
|
plainSecrets
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
async function push(
|
||||||
|
opts: GlobalOptions & { plainSecrets: boolean },
|
||||||
|
filePath: string,
|
||||||
|
remotePath: string
|
||||||
|
) {
|
||||||
const workspace = await resolveWorkspace(opts);
|
const workspace = await resolveWorkspace(opts);
|
||||||
await requireLogin(opts);
|
await requireLogin(opts);
|
||||||
|
|
||||||
if (!await validatePath(opts, remotePath)) {
|
if (!validatePath(remotePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +158,12 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
|||||||
|
|
||||||
console.log(colors.bold.yellow("Pushing variable..."));
|
console.log(colors.bold.yellow("Pushing variable..."));
|
||||||
|
|
||||||
await pushVariable(workspace.workspaceId, filePath, remotePath);
|
await pushVariable(
|
||||||
|
workspace.workspaceId,
|
||||||
|
filePath,
|
||||||
|
remotePath,
|
||||||
|
opts.plainSecrets
|
||||||
|
);
|
||||||
console.log(colors.bold.underline.green(`Variable ${remotePath} pushed`));
|
console.log(colors.bold.underline.green(`Variable ${remotePath} pushed`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,11 +171,12 @@ export async function pushVariable(
|
|||||||
workspace: string,
|
workspace: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
remotePath: string,
|
remotePath: string,
|
||||||
|
plainSecrets: boolean
|
||||||
) {
|
) {
|
||||||
const data = decoverto.type(VariableFile).rawToInstance(
|
const data = decoverto
|
||||||
await Deno.readTextFile(filePath),
|
.type(VariableFile)
|
||||||
);
|
.rawToInstance(await Deno.readTextFile(filePath));
|
||||||
await data.push(workspace, remotePath);
|
await data.push(workspace, remotePath, plainSecrets);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new Command()
|
const command = new Command()
|
||||||
@@ -173,9 +184,10 @@ const command = new Command()
|
|||||||
.action(list as any)
|
.action(list as any)
|
||||||
.command(
|
.command(
|
||||||
"push",
|
"push",
|
||||||
"Push a local variable spec. This overrides any remote versions.",
|
"Push a local variable spec. This overrides any remote versions."
|
||||||
)
|
)
|
||||||
.arguments("<file_path:string> <remote_path:string>")
|
.arguments("<file_path:string> <remote_path:string>")
|
||||||
|
.option("--plain-secrets", "Push secrets as plain text")
|
||||||
.action(push as any);
|
.action(push as any);
|
||||||
|
|
||||||
export default command;
|
export default command;
|
||||||
|
|||||||
144
cli/workspace.ts
144
cli/workspace.ts
@@ -30,7 +30,7 @@ export class Workspace {
|
|||||||
remote: string,
|
remote: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
name: string,
|
name: string,
|
||||||
token: string,
|
token: string
|
||||||
) {
|
) {
|
||||||
this.remote = remote;
|
this.remote = remote;
|
||||||
this.workspaceId = workspaceId;
|
this.workspaceId = workspaceId;
|
||||||
@@ -40,7 +40,7 @@ export class Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeWorkspaceStream(
|
function makeWorkspaceStream(
|
||||||
readable: ReadableStream<Uint8Array>,
|
readable: ReadableStream<Uint8Array>
|
||||||
): ReadableStream<Workspace> {
|
): ReadableStream<Workspace> {
|
||||||
return readable
|
return readable
|
||||||
.pipeThrough(new DelimiterStream(new TextEncoder().encode("\n")))
|
.pipeThrough(new DelimiterStream(new TextEncoder().encode("\n")))
|
||||||
@@ -59,7 +59,7 @@ function makeWorkspaceStream(
|
|||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export async function allWorkspaces(): Promise<Workspace[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getActiveWorkspaceName(
|
async function getActiveWorkspaceName(
|
||||||
opts: GlobalOptions,
|
opts: GlobalOptions
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
if (opts.workspace) {
|
if (opts.workspace) {
|
||||||
return opts.workspace;
|
return opts.workspace;
|
||||||
@@ -99,7 +99,7 @@ async function getActiveWorkspaceName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getActiveWorkspace(
|
export async function getActiveWorkspace(
|
||||||
opts: GlobalOptions,
|
opts: GlobalOptions
|
||||||
): Promise<Workspace | undefined> {
|
): Promise<Workspace | undefined> {
|
||||||
const name = await getActiveWorkspaceName(opts);
|
const name = await getActiveWorkspaceName(opts);
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -109,7 +109,7 @@ export async function getActiveWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkspaceByName(
|
export async function getWorkspaceByName(
|
||||||
workspaceName: string,
|
workspaceName: string
|
||||||
): Promise<Workspace | undefined> {
|
): Promise<Workspace | undefined> {
|
||||||
const workspaceStream = await getWorkspaceStream();
|
const workspaceStream = await getWorkspaceStream();
|
||||||
for await (const workspace of workspaceStream) {
|
for await (const workspace of workspaceStream) {
|
||||||
@@ -136,36 +136,40 @@ async function list(opts: GlobalOptions) {
|
|||||||
} else {
|
} else {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.render();
|
.render();
|
||||||
|
|
||||||
console.log('Active: ' + colors.green.bold(activeName || 'none'))
|
console.log("Active: " + colors.green.bold(activeName || "none"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function switchC(opts: GlobalOptions, workspaceName: string) {
|
async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||||
if (opts.workspace) {
|
if (opts.workspace) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red.bold(
|
colors.red.bold(
|
||||||
"! Workspace needs to be specified as positional argument, not as option.",
|
"! Workspace needs to be specified as positional argument, not as option."
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const all = await allWorkspaces();
|
const all = await allWorkspaces();
|
||||||
if (all.findIndex((x) => x.name === workspaceName) === -1) {
|
if (all.findIndex((x) => x.name === workspaceName) === -1) {
|
||||||
console.log(colors.red.bold(`! This workspace profile ${workspaceName} does not exist locally.`));
|
console.log(
|
||||||
console.log("available workspace profiles:")
|
colors.red.bold(
|
||||||
|
`! This workspace profile ${workspaceName} does not exist locally.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log("available workspace profiles:");
|
||||||
for (const w of all) {
|
for (const w of all) {
|
||||||
console.log(' - ' + w.name)
|
console.log(" - " + w.name);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Deno.writeTextFile(
|
return await Deno.writeTextFile(
|
||||||
(await getRootStore()) + "/activeWorkspace",
|
(await getRootStore()) + "/activeWorkspace",
|
||||||
workspaceName,
|
workspaceName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +181,13 @@ export async function add(
|
|||||||
},
|
},
|
||||||
workspaceName: string | undefined,
|
workspaceName: string | undefined,
|
||||||
workspaceId: string | undefined,
|
workspaceId: string | undefined,
|
||||||
remote: string | undefined,
|
remote: string | undefined
|
||||||
) {
|
) {
|
||||||
if (opts.workspace) {
|
if (opts.workspace) {
|
||||||
console.log(
|
console.log(
|
||||||
colors.red.bold(
|
colors.red.bold(
|
||||||
"! Workspace needs to be specified as positional argument, not as option.",
|
"! Workspace needs to be specified as positional argument, not as option."
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,7 +197,11 @@ export async function add(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
workspaceId = await Input.prompt({ message: "Enter the ID of this workspace", default: workspaceName, suggestions: [workspaceName] });
|
workspaceId = await Input.prompt({
|
||||||
|
message: "Enter the ID of this workspace",
|
||||||
|
default: workspaceName,
|
||||||
|
suggestions: [workspaceName],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
@@ -209,8 +217,8 @@ export async function add(
|
|||||||
await Input.prompt({
|
await Input.prompt({
|
||||||
message: "Enter the Remote URL",
|
message: "Enter the Remote URL",
|
||||||
suggestions: ["https://app.windmill.dev/"],
|
suggestions: ["https://app.windmill.dev/"],
|
||||||
default: "https://app.windmill.dev/"
|
default: "https://app.windmill.dev/",
|
||||||
}),
|
})
|
||||||
).toString();
|
).toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,22 +231,26 @@ export async function add(
|
|||||||
|
|
||||||
setClient(
|
setClient(
|
||||||
token,
|
token,
|
||||||
remote.endsWith("/") ? remote.substring(0, remote.length - 1) : remote,
|
remote.endsWith("/") ? remote.substring(0, remote.length - 1) : remote
|
||||||
);
|
);
|
||||||
let alreadyExists = false
|
let alreadyExists = false;
|
||||||
try {
|
try {
|
||||||
alreadyExists = await WorkspaceService.existsWorkspace({
|
alreadyExists = await WorkspaceService.existsWorkspace({
|
||||||
requestBody: { id: workspaceId },
|
requestBody: { id: workspaceId },
|
||||||
})
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(colors.red.bold("! Credentials or instance is invalid. Aborting."));
|
console.log(
|
||||||
throw e
|
colors.red.bold("! Credentials or instance is invalid. Aborting.")
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
if (opts.create) {
|
if (opts.create) {
|
||||||
if (
|
if (!alreadyExists) {
|
||||||
!alreadyExists
|
console.log(
|
||||||
) {
|
colors.yellow(
|
||||||
console.log(colors.yellow(`Workspace at id ${workspaceId} on ${remote} does not exist. Creating...`));
|
`Workspace at id ${workspaceId} on ${remote} does not exist. Creating...`
|
||||||
|
)
|
||||||
|
);
|
||||||
await WorkspaceService.createWorkspace({
|
await WorkspaceService.createWorkspace({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
@@ -248,63 +260,87 @@ export async function add(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (!alreadyExists) {
|
} else if (!alreadyExists) {
|
||||||
console.log(colors.red.bold(`! Workspace at id ${workspaceId} on ${remote} does not exist. Re-run with --create to create it. Aborting.`));
|
console.log(
|
||||||
console.log("On that instance and with those credentials, the workspaces that you can access are:")
|
colors.red.bold(
|
||||||
const workspaces = await WorkspaceService.listWorkspaces()
|
`! Workspace at id ${workspaceId} on ${remote} does not exist. Re-run with --create to create it. Aborting.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"On that instance and with those credentials, the workspaces that you can access are:"
|
||||||
|
);
|
||||||
|
const workspaces = await WorkspaceService.listWorkspaces();
|
||||||
for (const workspace of workspaces) {
|
for (const workspace of workspaces) {
|
||||||
console.log(`- ${workspace.id} (name: ${workspace.name})`)
|
console.log(`- ${workspace.id} (name: ${workspace.name})`);
|
||||||
}
|
}
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await addWorkspace({
|
await addWorkspace(
|
||||||
name: workspaceName,
|
{
|
||||||
remote: remote,
|
name: workspaceName,
|
||||||
workspaceId: workspaceId,
|
remote: remote,
|
||||||
token: token,
|
workspaceId: workspaceId,
|
||||||
}, opts);
|
token: token,
|
||||||
|
},
|
||||||
|
opts
|
||||||
|
);
|
||||||
await Deno.writeTextFile(
|
await Deno.writeTextFile(
|
||||||
(await getRootStore()) + "/activeWorkspace",
|
(await getRootStore()) + "/activeWorkspace",
|
||||||
workspaceName,
|
workspaceName
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
colors.green.underline(
|
||||||
|
`Added workspace ${workspaceName} for ${workspaceId} on ${remote}!`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
console.log(colors.green.underline(`Added workspace ${workspaceName} for ${workspaceId} on ${remote}!`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addWorkspace(workspace: Workspace, opts: any) {
|
export async function addWorkspace(workspace: Workspace, opts: any) {
|
||||||
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
|
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
|
||||||
|
await removeWorkspace(workspace.name, true, opts);
|
||||||
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
|
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
|
||||||
append: true,
|
append: true,
|
||||||
write: true,
|
write: true,
|
||||||
read: true,
|
read: true,
|
||||||
create: true,
|
create: true,
|
||||||
});
|
});
|
||||||
await removeWorkspace(workspace.name, true, opts);
|
|
||||||
await file.write(new TextEncoder().encode(JSON.stringify(workspace) + "\n"));
|
await file.write(new TextEncoder().encode(JSON.stringify(workspace) + "\n"));
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeWorkspace(name: string, silent: boolean, opts: any) {
|
export async function removeWorkspace(
|
||||||
|
name: string,
|
||||||
|
silent: boolean,
|
||||||
|
opts: any
|
||||||
|
) {
|
||||||
const orgWorkspaces = await allWorkspaces();
|
const orgWorkspaces = await allWorkspaces();
|
||||||
if (orgWorkspaces.findIndex((x) => x.name === name) === -1) {
|
if (orgWorkspaces.findIndex((x) => x.name === name) === -1) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.log(colors.red.bold(`! Workspace profile ${name} does not exist locally`));
|
console.log(
|
||||||
console.log("available workspace profiles:")
|
colors.red.bold(`! Workspace profile ${name} does not exist locally`)
|
||||||
await list(opts)
|
);
|
||||||
|
console.log("available workspace profiles:");
|
||||||
|
await list(opts);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (silent) {
|
if (silent) {
|
||||||
console.log(colors.yellow(`Replacing existing workspace ${name}`))
|
console.log(colors.yellow(`Replacing existing workspace ${name}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Deno.writeTextFile(
|
await Deno.writeTextFile(
|
||||||
(await getRootStore()) + "remotes.ndjson",
|
(await getRootStore()) + "remotes.ndjson",
|
||||||
orgWorkspaces
|
orgWorkspaces
|
||||||
.filter((x) => x.name !== name)
|
.filter((x) => x.name !== name)
|
||||||
.map((x) => JSON.stringify(x))
|
.map((x) => JSON.stringify(x))
|
||||||
.join("\n"),
|
.join("\n") + "\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
console.log(colors.green.underline(`Succesfully removed workspace ${name}!`));
|
console.log(
|
||||||
|
colors.green.underline(`Succesfully removed workspace ${name}!`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,10 +349,10 @@ async function remove(_opts: GlobalOptions, name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function whoami(_opts: GlobalOptions) {
|
async function whoami(_opts: GlobalOptions) {
|
||||||
await requireLogin(_opts)
|
await requireLogin(_opts);
|
||||||
console.log(await UserService.globalWhoami())
|
console.log(await UserService.globalWhoami());
|
||||||
const activeName = await getActiveWorkspaceName(_opts);
|
const activeName = await getActiveWorkspaceName(_opts);
|
||||||
console.log('Active: ' + colors.green.bold(activeName || 'none'))
|
console.log("Active: " + colors.green.bold(activeName || "none"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new Command()
|
const command = new Command()
|
||||||
@@ -333,14 +369,14 @@ const command = new Command()
|
|||||||
.option("-c --create", "Create the workspace if it does not exist")
|
.option("-c --create", "Create the workspace if it does not exist")
|
||||||
.option(
|
.option(
|
||||||
"--create-workspace-name <workspace_name:string>",
|
"--create-workspace-name <workspace_name:string>",
|
||||||
"Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.",
|
"Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id."
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--create-username <username:string>",
|
"--create-username <username:string>",
|
||||||
"Specify your own username in the newly created workspace. Ignored if --create is not specified or the workspace already exists.",
|
"Specify your own username in the newly created workspace. Ignored if --create is not specified or the workspace already exists.",
|
||||||
{
|
{
|
||||||
default: "admin",
|
default: "admin",
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.action(add as any)
|
.action(add as any)
|
||||||
.command("remove")
|
.command("remove")
|
||||||
|
|||||||
@@ -1,41 +1,59 @@
|
|||||||
import { JobService, ResourceService, VariableService } from './windmill-api/index.ts'
|
import {
|
||||||
import { OpenAPI } from './windmill-api/index.ts'
|
JobService,
|
||||||
|
ResourceService,
|
||||||
|
VariableService,
|
||||||
|
} from "./windmill-api/index.ts";
|
||||||
|
import { OpenAPI } from "./windmill-api/index.ts";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AdminService, AuditService, FlowService, GranularAclService, GroupService,
|
AdminService,
|
||||||
JobService, ResourceService, VariableService, ScriptService, ScheduleService, SettingsService,
|
AuditService,
|
||||||
UserService, WorkspaceService
|
FlowService,
|
||||||
} from './windmill-api/index.ts'
|
GranularAclService,
|
||||||
|
GroupService,
|
||||||
|
JobService,
|
||||||
|
ResourceService,
|
||||||
|
VariableService,
|
||||||
|
ScriptService,
|
||||||
|
ScheduleService,
|
||||||
|
SettingsService,
|
||||||
|
UserService,
|
||||||
|
WorkspaceService,
|
||||||
|
} from "./windmill-api/index.ts";
|
||||||
|
|
||||||
// @ts-ignore: Otherwise BigInt is not supported for export
|
// @ts-ignore: Otherwise BigInt is not supported for export
|
||||||
BigInt.prototype.toJSON = function () {
|
BigInt.prototype.toJSON = function () {
|
||||||
return this.toString();
|
return this.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
export { pgSql, pgClient } from './pg.ts'
|
export { pgSql, pgClient } from "./pg.ts";
|
||||||
|
|
||||||
export type Sql = string
|
export type Sql = string;
|
||||||
export type Email = string
|
export type Email = string;
|
||||||
export type Base64 = string
|
export type Base64 = string;
|
||||||
export type Resource<S extends string> = any
|
export type Resource<S extends string> = any;
|
||||||
|
|
||||||
export const SHARED_FOLDER = '/shared'
|
export const SHARED_FOLDER = "/shared";
|
||||||
|
|
||||||
export function setClient(token: string, baseUrl: string) {
|
export function setClient(token: string, baseUrl: string) {
|
||||||
OpenAPI.WITH_CREDENTIALS = true
|
OpenAPI.WITH_CREDENTIALS = true;
|
||||||
OpenAPI.TOKEN = token
|
OpenAPI.TOKEN = token;
|
||||||
OpenAPI.BASE = baseUrl + '/api'
|
OpenAPI.BASE = baseUrl + "/api";
|
||||||
}
|
}
|
||||||
|
|
||||||
setClient(Deno.env.get("WM_TOKEN") ?? 'no_token', Deno.env.get("BASE_INTERNAL_URL") ?? Deno.env.get("BASE_URL") ?? 'http://localhost:8000')
|
setClient(
|
||||||
|
Deno.env.get("WM_TOKEN") ?? "no_token",
|
||||||
|
Deno.env.get("BASE_INTERNAL_URL") ??
|
||||||
|
Deno.env.get("BASE_URL") ??
|
||||||
|
"http://localhost:8000"
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a client configuration from env variables
|
* Create a client configuration from env variables
|
||||||
* @returns client configuration
|
* @returns client configuration
|
||||||
*/
|
*/
|
||||||
export function getWorkspace(): string {
|
export function getWorkspace(): string {
|
||||||
return Deno.env.get("WM_WORKSPACE") ?? 'no_workspace'
|
return Deno.env.get("WM_WORKSPACE") ?? "no_workspace";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,27 +62,55 @@ export function getWorkspace(): string {
|
|||||||
* @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
|
* @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
|
||||||
* @returns resource value
|
* @returns resource value
|
||||||
*/
|
*/
|
||||||
export async function getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any> {
|
export async function getResource(
|
||||||
const workspace = getWorkspace()
|
path?: string,
|
||||||
path = path ?? getStatePath()
|
undefinedIfEmpty?: boolean
|
||||||
try {
|
): Promise<any> {
|
||||||
const resource = await ResourceService.getResource({ workspace, path })
|
const workspace = getWorkspace();
|
||||||
return await _transformLeaf(resource.value)
|
path = path ?? getStatePath();
|
||||||
} catch (e: any) {
|
try {
|
||||||
if (undefinedIfEmpty && e.status === 404) {
|
const resource = await ResourceService.getResource({ workspace, path });
|
||||||
return undefined
|
return await _transformLeaf(resource.value);
|
||||||
} else {
|
} catch (e: any) {
|
||||||
throw Error(`Resource not found at ${path} or not visible to you`)
|
if (undefinedIfEmpty && e.status === 404) {
|
||||||
}
|
return undefined;
|
||||||
|
} else {
|
||||||
|
throw Error(`Resource not found at ${path} or not visible to you`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full resource value by path
|
||||||
|
* @param path path of the resource, default to internal state path
|
||||||
|
* @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
|
||||||
|
* @returns full resource
|
||||||
|
*/
|
||||||
|
export async function getFullResource(
|
||||||
|
path?: string,
|
||||||
|
undefinedIfEmpty?: boolean
|
||||||
|
): Promise<any> {
|
||||||
|
const workspace = getWorkspace();
|
||||||
|
path = path ?? getStatePath();
|
||||||
|
try {
|
||||||
|
const resource = await ResourceService.getResource({ workspace, path });
|
||||||
|
const value = await _transformLeaf(resource.value);
|
||||||
|
return { ...resource, value };
|
||||||
|
} catch (e: any) {
|
||||||
|
if (undefinedIfEmpty && e.status === 404) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
throw Error(`Resource not found at ${path} or not visible to you`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStatePath(): string {
|
export function getStatePath(): string {
|
||||||
const state_path = Deno.env.get("WM_STATE_PATH")
|
const state_path = Deno.env.get("WM_STATE_PATH");
|
||||||
if (state_path === undefined) {
|
if (state_path === undefined) {
|
||||||
throw Error("State path not set")
|
throw Error("State path not set");
|
||||||
}
|
}
|
||||||
return state_path
|
return state_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,16 +119,29 @@ export function getStatePath(): string {
|
|||||||
* @param value new value of the resource to set
|
* @param value new value of the resource to set
|
||||||
* @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
|
* @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
|
||||||
*/
|
*/
|
||||||
export async function setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void> {
|
export async function setResource(
|
||||||
path = path ?? getStatePath()
|
value: any,
|
||||||
const workspace = getWorkspace()
|
path?: string,
|
||||||
if (await ResourceService.existsResource({ workspace, path })) {
|
initializeToTypeIfNotExist?: string
|
||||||
await ResourceService.updateResourceValue({ workspace, path, requestBody: { value } })
|
): Promise<void> {
|
||||||
} else if (initializeToTypeIfNotExist) {
|
path = path ?? getStatePath();
|
||||||
await ResourceService.createResource({ workspace, requestBody: { path, value, resource_type: initializeToTypeIfNotExist } })
|
const workspace = getWorkspace();
|
||||||
} else {
|
if (await ResourceService.existsResource({ workspace, path })) {
|
||||||
throw Error(`Resource at path ${path} does not exist and no type was provided to initialize it`)
|
await ResourceService.updateResourceValue({
|
||||||
}
|
workspace,
|
||||||
|
path,
|
||||||
|
requestBody: { value },
|
||||||
|
});
|
||||||
|
} else if (initializeToTypeIfNotExist) {
|
||||||
|
await ResourceService.createResource({
|
||||||
|
workspace,
|
||||||
|
requestBody: { path, value, resource_type: initializeToTypeIfNotExist },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error(
|
||||||
|
`Resource at path ${path} does not exist and no type was provided to initialize it`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,7 +150,7 @@ export async function setResource(value: any, path?: string, initializeToTypeIfN
|
|||||||
* @deprecated use setState instead
|
* @deprecated use setState instead
|
||||||
*/
|
*/
|
||||||
export async function setInternalState(state: any): Promise<void> {
|
export async function setInternalState(state: any): Promise<void> {
|
||||||
await setResource(state, undefined, 'state')
|
await setResource(state, undefined, "state");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,39 +158,41 @@ export async function setInternalState(state: any): Promise<void> {
|
|||||||
* @param state state to set
|
* @param state state to set
|
||||||
*/
|
*/
|
||||||
export async function setState(state: any): Promise<void> {
|
export async function setState(state: any): Promise<void> {
|
||||||
await setResource(state, undefined, 'state')
|
await setResource(state, undefined, "state");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the shared state
|
* Set the shared state
|
||||||
* @param state state to set
|
* @param state state to set
|
||||||
*/
|
*/
|
||||||
export async function setSharedState(state: any, path = 'state.json'): Promise<void> {
|
export async function setSharedState(
|
||||||
await Deno.writeTextFile(SHARED_FOLDER + '/' + path, JSON.stringify(state))
|
state: any,
|
||||||
|
path = "state.json"
|
||||||
|
): Promise<void> {
|
||||||
|
await Deno.writeTextFile(SHARED_FOLDER + "/" + path, JSON.stringify(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the shared state
|
* Get the shared state
|
||||||
* @param state state to set
|
* @param state state to set
|
||||||
*/
|
*/
|
||||||
export async function getSharedState(path = 'state.json'): Promise<any> {
|
export async function getSharedState(path = "state.json"): Promise<any> {
|
||||||
return JSON.parse(await Deno.readTextFile(SHARED_FOLDER + '/' + path))
|
return JSON.parse(await Deno.readTextFile(SHARED_FOLDER + "/" + path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the internal state
|
* Get the internal state
|
||||||
* @deprecated use getState instead
|
* @deprecated use getState instead
|
||||||
*/
|
*/
|
||||||
export async function getInternalState(): Promise<any> {
|
export async function getInternalState(): Promise<any> {
|
||||||
return await getResource(getStatePath(), true)
|
return await getResource(getStatePath(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the state shared across executions
|
* Get the state shared across executions
|
||||||
*/
|
*/
|
||||||
export async function getState(): Promise<any> {
|
export async function getState(): Promise<any> {
|
||||||
return await getResource(getStatePath(), true)
|
return await getResource(getStatePath(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,38 +201,43 @@ export async function getState(): Promise<any> {
|
|||||||
* @returns variable value
|
* @returns variable value
|
||||||
*/
|
*/
|
||||||
export async function getVariable(path: string): Promise<string | undefined> {
|
export async function getVariable(path: string): Promise<string | undefined> {
|
||||||
const workspace = getWorkspace()
|
const workspace = getWorkspace();
|
||||||
try {
|
try {
|
||||||
const variable = await VariableService.getVariable({ workspace, path })
|
const variable = await VariableService.getVariable({ workspace, path });
|
||||||
return variable.value
|
return variable.value;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw Error(`Variable not found at ${path} or not visible to you`)
|
throw Error(`Variable not found at ${path} or not visible to you`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transformLeaves(d: { [key: string]: any }): Promise<{ [key: string]: any }> {
|
async function transformLeaves(d: {
|
||||||
for (const k in d) {
|
[key: string]: any;
|
||||||
d[k] = await _transformLeaf(d[k])
|
}): Promise<{ [key: string]: any }> {
|
||||||
}
|
for (const k in d) {
|
||||||
return d
|
d[k] = await _transformLeaf(d[k]);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VAR_RESOURCE_PREFIX = "$var:"
|
const VAR_RESOURCE_PREFIX = "$var:";
|
||||||
|
const RES_RESOURCE_PREFIX = "$res:";
|
||||||
async function _transformLeaf(v: any): Promise<any> {
|
async function _transformLeaf(v: any): Promise<any> {
|
||||||
if (typeof v === 'object') {
|
if (typeof v === "object") {
|
||||||
return transformLeaves(v)
|
return transformLeaves(v);
|
||||||
}
|
} else if (typeof v === "string" && v.startsWith(VAR_RESOURCE_PREFIX)) {
|
||||||
else if (typeof v === 'string' && v.startsWith(VAR_RESOURCE_PREFIX)) {
|
const varName = v.substring(VAR_RESOURCE_PREFIX.length);
|
||||||
const varName = v.substring(VAR_RESOURCE_PREFIX.length)
|
return await getVariable(varName);
|
||||||
return await getVariable(varName)
|
} else if (typeof v === "string" && v.startsWith(RES_RESOURCE_PREFIX)) {
|
||||||
} else {
|
const resName = v.substring(RES_RESOURCE_PREFIX.length);
|
||||||
return v
|
return await getResource(resName);
|
||||||
}
|
} else {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function databaseUrlFromResource(path: string): Promise<string> {
|
export async function databaseUrlFromResource(path: string): Promise<string> {
|
||||||
const resource = await getResource(path)
|
const resource = await getResource(path);
|
||||||
return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}`
|
return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,78 +246,84 @@ export async function databaseUrlFromResource(path: string): Promise<string> {
|
|||||||
* @returns approval page UI URL, resume and cancel API URLs for resumeing the flow
|
* @returns approval page UI URL, resume and cancel API URLs for resumeing the flow
|
||||||
*/
|
*/
|
||||||
export async function getResumeUrls(approver?: string): Promise<{
|
export async function getResumeUrls(approver?: string): Promise<{
|
||||||
approvalPage: string;
|
approvalPage: string;
|
||||||
resume: string;
|
resume: string;
|
||||||
cancel: string;
|
cancel: string;
|
||||||
}> {
|
}> {
|
||||||
const nonce = Math.floor(Math.random() * 4294967295);
|
const nonce = Math.floor(Math.random() * 4294967295);
|
||||||
const workspace = getWorkspace()
|
const workspace = getWorkspace();
|
||||||
return await JobService.getResumeUrls({ workspace, resumeId: nonce, approver, id: Deno.env.get("WM_JOB_ID") ?? 'NO_JOB_ID' })
|
return await JobService.getResumeUrls({
|
||||||
|
workspace,
|
||||||
|
resumeId: nonce,
|
||||||
|
approver,
|
||||||
|
id: Deno.env.get("WM_JOB_ID") ?? "NO_JOB_ID",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use getResumeUrls instead
|
* @deprecated use getResumeUrls instead
|
||||||
*/
|
*/
|
||||||
export function getResumeEndpoints(approver?: string): Promise<{
|
export function getResumeEndpoints(approver?: string): Promise<{
|
||||||
approvalPage: string;
|
approvalPage: string;
|
||||||
resume: string;
|
resume: string;
|
||||||
cancel: string;
|
cancel: string;
|
||||||
}> {
|
}> {
|
||||||
return getResumeUrls(approver)
|
return getResumeUrls(approver);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64ToUint8Array(data: string): Uint8Array {
|
export function base64ToUint8Array(data: string): Uint8Array {
|
||||||
return Uint8Array.from(atob(data), c => c.charCodeAt(0))
|
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
|
export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
|
||||||
let base64 = ''
|
let base64 = "";
|
||||||
const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
const encodings =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
|
||||||
const bytes = new Uint8Array(arrayBuffer)
|
const bytes = new Uint8Array(arrayBuffer);
|
||||||
const byteLength = bytes.byteLength
|
const byteLength = bytes.byteLength;
|
||||||
const byteRemainder = byteLength % 3
|
const byteRemainder = byteLength % 3;
|
||||||
const mainLength = byteLength - byteRemainder
|
const mainLength = byteLength - byteRemainder;
|
||||||
|
|
||||||
let a, b, c, d
|
let a, b, c, d;
|
||||||
let chunk
|
let chunk;
|
||||||
|
|
||||||
// Main loop deals with bytes in chunks of 3
|
// Main loop deals with bytes in chunks of 3
|
||||||
for (let i = 0; i < mainLength; i = i + 3) {
|
for (let i = 0; i < mainLength; i = i + 3) {
|
||||||
// Combine the three bytes into a single integer
|
// Combine the three bytes into a single integer
|
||||||
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
|
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
||||||
|
|
||||||
// Use bitmasks to extract 6-bit segments from the triplet
|
// Use bitmasks to extract 6-bit segments from the triplet
|
||||||
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
|
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
|
||||||
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
|
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
|
||||||
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
|
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
|
||||||
d = chunk & 63 // 63 = 2^6 - 1
|
d = chunk & 63; // 63 = 2^6 - 1
|
||||||
|
|
||||||
// Convert the raw binary segments to the appropriate ASCII encoding
|
// Convert the raw binary segments to the appropriate ASCII encoding
|
||||||
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
|
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deal with the remaining bytes and padding
|
// Deal with the remaining bytes and padding
|
||||||
if (byteRemainder == 1) {
|
if (byteRemainder == 1) {
|
||||||
chunk = bytes[mainLength]
|
chunk = bytes[mainLength];
|
||||||
|
|
||||||
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
|
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
|
||||||
|
|
||||||
// Set the 4 least significant bits to zero
|
// Set the 4 least significant bits to zero
|
||||||
b = (chunk & 3) << 4 // 3 = 2^2 - 1
|
b = (chunk & 3) << 4; // 3 = 2^2 - 1
|
||||||
|
|
||||||
base64 += encodings[a] + encodings[b] + '=='
|
base64 += encodings[a] + encodings[b] + "==";
|
||||||
} else if (byteRemainder == 2) {
|
} else if (byteRemainder == 2) {
|
||||||
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
|
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
|
||||||
|
|
||||||
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
|
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
|
||||||
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
|
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
|
||||||
|
|
||||||
// Set the 2 least significant bits to zero
|
// Set the 2 least significant bits to zero
|
||||||
c = (chunk & 15) << 2 // 15 = 2^4 - 1
|
c = (chunk & 15) << 2; // 15 = 2^4 - 1
|
||||||
|
|
||||||
base64 += encodings[a] + encodings[b] + encodings[c] + '='
|
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64
|
return base64;
|
||||||
}
|
}
|
||||||
|
|||||||
86
docker-compose-dev.yml
Normal file
86
docker-compose-dev.yml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:14
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
expose:
|
||||||
|
- 5432
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: windmill
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
windmill_server:
|
||||||
|
image: ghcr.io/windmill-labs/windmill:main
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||||
|
- BASE_URL=http://${WM_BASE_URL}
|
||||||
|
- RUST_LOG=info
|
||||||
|
## You can set the number of workers to > 0 and not need any separate worker service
|
||||||
|
- NUM_WORKERS=0
|
||||||
|
- DISABLE_SERVER=false
|
||||||
|
- METRICS_ADDR=false
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
# volumes:
|
||||||
|
# - ./oauth.json/:/usr/src/app/oauth.json
|
||||||
|
|
||||||
|
windmill_worker:
|
||||||
|
image: ghcr.io/windmill-labs/windmill:main
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||||
|
- BASE_URL=http://${WM_BASE_URL}
|
||||||
|
- BASE_INTERNAL_URL=http://windmill_server:8000
|
||||||
|
- RUST_LOG=info
|
||||||
|
- NUM_WORKERS=1
|
||||||
|
- DISABLE_SERVER=true
|
||||||
|
- KEEP_JOB_DIR=false
|
||||||
|
- DENO_PATH=/usr/bin/deno
|
||||||
|
- PYTHON_PATH=/usr/local/bin/python3
|
||||||
|
- METRICS_ADDR=false
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
|
||||||
|
volumes:
|
||||||
|
- worker_dependency_cache:/tmp/windmill/cache
|
||||||
|
|
||||||
|
lsp:
|
||||||
|
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- 3001
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.5.2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
environment:
|
||||||
|
- BASE_URL=${WM_BASE_URL}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data: null
|
||||||
|
worker_dependency_cache: null
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
version: '3.7'
|
version: "3.7"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/postgresql/data
|
- db_data:/var/lib/postgresql/data
|
||||||
ports:
|
expose:
|
||||||
- 5432:5432
|
- 5432
|
||||||
# NOTE: if port 5432 are already in use locally, we'll also bind another port
|
|
||||||
- 5433:5432
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_DB: windmill
|
POSTGRES_DB: windmill
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -25,11 +22,11 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
expose:
|
||||||
- 8000:8000
|
- 8000
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||||
- BASE_URL=http://${WM_BASE_URL}
|
- BASE_URL=${WM_BASE_URL}
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
## You can set the number of workers to > 0 and not need any separate worker service
|
## You can set the number of workers to > 0 and not need any separate worker service
|
||||||
- NUM_WORKERS=0
|
- NUM_WORKERS=0
|
||||||
@@ -39,8 +36,8 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./oauth.json/:/usr/src/app/oauth.json
|
# - ./oauth.json/:/usr/src/app/oauth.json
|
||||||
|
|
||||||
windmill_worker:
|
windmill_worker:
|
||||||
image: ghcr.io/windmill-labs/windmill:main
|
image: ghcr.io/windmill-labs/windmill:main
|
||||||
deploy:
|
deploy:
|
||||||
@@ -48,7 +45,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||||
- BASE_URL=http://${WM_BASE_URL}
|
- BASE_URL=${WM_BASE_URL}
|
||||||
- BASE_INTERNAL_URL=http://windmill_server:8000
|
- BASE_INTERNAL_URL=http://windmill_server:8000
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
- NUM_WORKERS=1
|
- NUM_WORKERS=1
|
||||||
@@ -67,8 +64,8 @@ services:
|
|||||||
lsp:
|
lsp:
|
||||||
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
expose:
|
||||||
- 3001:3001
|
- 3001
|
||||||
|
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2.5.2-alpine
|
image: caddy:2.5.2-alpine
|
||||||
@@ -79,7 +76,7 @@ services:
|
|||||||
- 80:80
|
- 80:80
|
||||||
- 443:443
|
- 443:443
|
||||||
environment:
|
environment:
|
||||||
- BASE_URL=${WM_BASE_URL}
|
- BASE_URL=${CADDY_REVERSE_PROXY}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data: null
|
db_data: null
|
||||||
|
|||||||
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_DEFAULT_WEBHOOK_TYPE=async
|
||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -7,3 +7,4 @@ node_modules
|
|||||||
CaddyfileRemoteRuben
|
CaddyfileRemoteRuben
|
||||||
tests-out/
|
tests-out/
|
||||||
storageState.json
|
storageState.json
|
||||||
|
.env.production
|
||||||
@@ -2,9 +2,11 @@ http://localhost {
|
|||||||
bind {$ADDRESS}
|
bind {$ADDRESS}
|
||||||
reverse_proxy /api/* http://localhost:8000
|
reverse_proxy /api/* http://localhost:8000
|
||||||
reverse_proxy /* http://localhost:3000
|
reverse_proxy /* http://localhost:3000
|
||||||
|
reverse_proxy /ws/* http://localhost:3001
|
||||||
}
|
}
|
||||||
|
|
||||||
https://localhost {
|
https://localhost {
|
||||||
bind {$ADDRESS}
|
bind {$ADDRESS}
|
||||||
|
reverse_proxy /api/* http://localhost:8000
|
||||||
reverse_proxy /ws/* http://localhost:3001
|
reverse_proxy /ws/* http://localhost:3001
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
:8080 {
|
:8080 {
|
||||||
bind {$ADDRESS}
|
bind {$ADDRESS}
|
||||||
reverse_proxy /api/* http://windmill:8000
|
reverse_proxy /api/* http://localhost:8000
|
||||||
reverse_proxy /* http://localhost:3000
|
reverse_proxy /* http://localhost:3000
|
||||||
reverse_proxy /ws/* http://lsp:3001 {
|
reverse_proxy /ws/* http://lsp:3001 {
|
||||||
lb_policy header "Authorization"
|
lb_policy header "Authorization"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ In the root folder:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build . -t windmill
|
docker build . -t windmill
|
||||||
docker-compose up db windmill_server windmill_worker
|
docker compose -f docker-compose-dev.yml up db windmill_server windmill_worker
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Backend is run by cargo
|
### 2. Backend is run by cargo
|
||||||
@@ -28,20 +28,23 @@ docker-compose up db windmill_server windmill_worker
|
|||||||
- Install llvm
|
- Install llvm
|
||||||
|
|
||||||
**On OSX:**
|
**On OSX:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install llvm caddy gsed
|
brew install llvm caddy gsed
|
||||||
|
|
||||||
# make LLVM tools available on PATH
|
# make LLVM tools available on PATH
|
||||||
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
|
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
|
||||||
|
|
||||||
# now, restart your shell. You should now have the `lld` binary on your PATH.
|
# now, restart your shell. You should now have the `lld` binary on your PATH.
|
||||||
```
|
```
|
||||||
|
|
||||||
- To test that you have Rust and Cargo installed run `cargo --version`
|
- To test that you have Rust and Cargo installed run `cargo --version`
|
||||||
|
|
||||||
- In your terminal, go to the backend directory and run `cargo build`
|
- In your terminal, go to the backend directory and run `cargo build`
|
||||||
- Run `cargo run`
|
- Run `cargo run`
|
||||||
|
|
||||||
**Known issue on M1 Mac while running `cargo build`**
|
**Known issue on M1 Mac while running `cargo build`**
|
||||||
|
|
||||||
- You may encounter `linking with cc failed` build time error.
|
- You may encounter `linking with cc failed` build time error.
|
||||||
- To solve this run:
|
- To solve this run:
|
||||||
```bash
|
```bash
|
||||||
@@ -49,7 +52,6 @@ docker-compose up db windmill_server windmill_worker
|
|||||||
source ~/.zshrc
|
source ~/.zshrc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
**Do a Frontend Build**
|
**Do a Frontend Build**
|
||||||
|
|
||||||
In order to run the backend, you need to have a frontend build inside `frontend/build/`.
|
In order to run the backend, you need to have a frontend build inside `frontend/build/`.
|
||||||
@@ -70,11 +72,14 @@ npm run build
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Known issue while running `npm run build`**
|
**Known issue while running `npm run build`**
|
||||||
|
|
||||||
- You may encounter `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` error.
|
- You may encounter `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` error.
|
||||||
- To solve this run:
|
- To solve this run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export NODE_OPTIONS=--max_old_space_size=8096
|
export NODE_OPTIONS=--max_old_space_size=8096
|
||||||
```
|
```
|
||||||
|
|
||||||
- run `npm run build` again
|
- run `npm run build` again
|
||||||
|
|
||||||
In the root folder:
|
In the root folder:
|
||||||
@@ -86,7 +91,7 @@ docker-compose up db
|
|||||||
In the backend folder:
|
In the backend folder:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5433/windmill?sslmode=disable cargo run
|
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now access [http://127.0.0.1:8000](http://127.0.0.1:8000).
|
You can now access [http://127.0.0.1:8000](http://127.0.0.1:8000).
|
||||||
@@ -145,3 +150,26 @@ Recommended config for VS Code:
|
|||||||
```
|
```
|
||||||
|
|
||||||
- turn _format on save_ on
|
- turn _format on save_ on
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
The project is built with [SvelteKit](https://kit.svelte.dev/) and uses as output static files.
|
||||||
|
There are others adapters for sveltekit, but we use the static adapter.
|
||||||
|
|
||||||
|
To build the frontend as static assets, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The output is in the `build` folder.
|
||||||
|
|
||||||
|
The default build assume you serve every non static files as the 200.html file which is catchall. If you prefer a normal layout, you can use:
|
||||||
|
|
||||||
|
```
|
||||||
|
NOTCATCHALL=true npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
which will generate an index.html and allow you to serve the frontend with any static server.
|
||||||
|
|
||||||
|
Env variables used for build are set in .env file. See [https://vitejs.dev/guide/env-and-mode.html#env-files](https://vitejs.dev/guide/env-and-mode.html#env-files) for more details.
|
||||||
|
|||||||
1
frontend/copy_workers.sh
Executable file
1
frontend/copy_workers.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
cp -r node_modules/monaco-editor-workers/dist/workers static
|
||||||
5546
frontend/package-lock.json
generated
5546
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user