feat: add gcp trigger (#5501)

* update: add migration for gcp_trigger table, add cli method gcp and add gcp_trigger args for github ci

* update: add gcp module and routing it in lib.rs

* update: added gcp type to TRIGGER KIND type in db, add new feature condition to try_get_fn and added gcp_trigger feature in cargo file

* update: add ui for gcp trigger, added missing triggers type for job kin, add gcp trigger in deploy to functionallity

* adding google cloud crate

* update: handle pull and push delivery

* update: handle pull and push delivery

* update: handle push event, changed front ui

* update: ui

* fix: capture for gcp

* update: automatically manage pub sub subscription and refactoring

* capture done

* update cli types

* fix: wrong func argument and type openapi

* fix: missing import

* update .sqlx

* update: svg color and remove pulse button for capture push gcp

* update: handle deployto

* update script helper and link to hub gcp script/flow

* update gcp representation

* update: add confirmation modal

* add unique index to mitigate same subscriber id, fix `zombie worker`, update test connection logic

* Update frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update cli/gen/services.gen.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* nits

* update repo ref

* fix feature function

* update dependencies

* update .sqlx

* fix missing import

* update: handle existing subscription and creating new one or update it

* fix ee build

* handle existing config properly

* update .sqlx

* update .sqlx

* remove unused

* update documentation link fix ci

* use on instance of empty_string_as_none fn

* update refo ref

* updat script_helper

* update ui

* update migration

* add missing arguments

* fix and nits

* update repo ref

* remove unused

* nits

* add doc links

* nits

* Update frontend/src/lib/components/triggers/gcp/GcpTriggerPanel.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update frontend/src/lib/components/triggers/gcp/GcpTriggerEditorInner.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* use trigger path as default route path

* update .sqlx

* update repo ref

* update ref

* update route path

* update types

* fix deploy to

* nits

* update repo ref

* update .sqlx

* Update frontend/src/lib/components/triggers/gcp/GcpTriggerEditorConfigSection.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* nits

* nits

* nits

* remove dupliacte fn and update repo ref

* remove unused import

* fix missing import

* fix: wong name var

* update script helper and template script

* Update frontend/src/lib/script_helpers.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update ee-repo-ref.txt

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
dieriba
2025-04-10 22:39:01 +02:00
committed by GitHub
parent 785da73abf
commit 6339775404
106 changed files with 4351 additions and 332 deletions
+2 -2
View File
@@ -64,7 +64,7 @@ jobs:
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
@@ -81,7 +81,7 @@ jobs:
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,nats,sqs_trigger,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
- name: Rename binary with corresponding architecture
run: |
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"
+2 -2
View File
@@ -95,7 +95,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=embedding,parquet,openidconnect,jemalloc,license,http_trigger,zip,oauth2,dind,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,agent_worker_server,all_languages
features=embedding,parquet,openidconnect,jemalloc,license,http_trigger,zip,oauth2,dind,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,agent_worker_server,all_languages
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
${{ steps.meta-public.outputs.tags }}
@@ -157,7 +157,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,agent_worker_server,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,agent_worker_server,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,otel,dind,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}
${{ steps.meta-ee-public.outputs.tags }}
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,license,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,mqtt_trigger,gcp_trigger,websocket,smtp,static_frontend,all_languages
- name: Rename binary with corresponding architecture
run: |
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"
@@ -0,0 +1,128 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "gcp_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "subscription_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "topic_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "delivery_type: _",
"type_info": {
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
}
},
{
"ordinal": 5,
"name": "delivery_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"name": "enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
false
]
},
"hash": "0102308ffa1c0dbfba54d29246535bb81146a4cfae0ec408435570e0813a3bef"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger \n SET \n enabled = $1, \n email = $2, \n edited_by = $3, \n edited_at = now(), \n server_id = NULL, \n error = NULL\n WHERE \n path = $4 AND \n workspace_id = $5 \n RETURNING 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "070b8ad0b59f485fa5bf68082b060f5c3561c37e9c6f2834d234a862a475a6eb"
}
@@ -0,0 +1,126 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM\n gcp_trigger\n WHERE\n delivery_type != 'push'::DELIVERY_MODE AND\n enabled IS TRUE\n AND (last_server_ping IS NULL OR\n last_server_ping < now() - interval '15 seconds'\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "gcp_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "subscription_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "topic_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "delivery_type: _",
"type_info": {
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
}
},
{
"ordinal": 5,
"name": "delivery_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"name": "enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
false
]
},
"hash": "15fbe481789a7817bf37415fb935f9ed537fd7d3b266d928af4d5d3dd8bb5c18"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT trigger_config as \"trigger_config: _\", owner, email\n FROM capture_config\n WHERE workspace_id = $1 AND path = $2 AND is_flow = $3 AND trigger_kind = $4 AND last_client_ping > NOW() - INTERVAL '10 seconds'",
"query": "\n SELECT \n trigger_config AS \"trigger_config: _\", \n owner, \n email\n FROM \n capture_config\n WHERE \n workspace_id = $1\n AND path = $2\n AND is_flow = $3\n AND trigger_kind = $4\n AND last_client_ping > NOW() - INTERVAL '10 seconds'\n AND (\n $5::bool IS FALSE\n OR (\n trigger_config IS NOT NULL\n AND trigger_config ->> 'delivery_type' = 'push'\n )\n )\n ",
"describe": {
"columns": [
{
@@ -37,11 +37,13 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
}
},
"Bool"
]
},
"nullable": [
@@ -50,5 +52,5 @@
false
]
},
"hash": "e23e110e1f0438d21534fc4323e0e7bc1f0dbeca2e4f44ced05bae0ca5ca1039"
"hash": "23419adcd74c326d716527293eff518b42f4cdb33e034441015494bd26c172d2"
}
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n capture_config \n SET \n server_id = $1,\n last_server_ping = now(), \n error = 'Connecting...' \n WHERE \n last_client_ping > NOW() - INTERVAL '10 seconds' AND \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'gcp' AND \n (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') \n RETURNING true\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Bool"
]
},
"nullable": [
null
]
},
"hash": "299e16725162888c01712f371785199960264b54c1ddf928c0c654ab15176f63"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT \n 1 \n FROM \n gcp_trigger \n WHERE \n path = $1 AND \n workspace_id = $2\n )",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "33ee913ce263600a3f94f90e4a42cf0e4086030f3b7994e4892392765cbe1517"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n capture\n SET \n path = $1\n WHERE \n path = $2 \n AND workspace_id = $3 \n AND is_flow = $4\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "37011e7f4cdfc87294e44252cca4f4683a12b82b972842f88f5c01111580224d"
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id, \n path, \n script_path, \n is_flow, \n email, \n enabled, \n edited_by\n ) \n VALUES (\n $1, \n $2, \n $3, \n $4,\n $5,\n $6, \n $7, \n $8, \n $9,\n $10,\n $11,\n $12\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "3f5520e0ea00569bf169da9abde31617043fcc0bea3ef62c50fcd881f3d48605"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM capture WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "41e557e1b63b13c9fcc195901c0bd0de7e03c539ee046955543d9693551246f7"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE capture_config SET last_client_ping = now() WHERE workspace_id = $1 AND path = $2 AND is_flow = $3 AND trigger_kind = $4",
"query": "\n UPDATE \n capture_config\n SET \n last_client_ping = NOW()\n WHERE \n workspace_id = $1 \n AND path = $2 \n AND is_flow = $3 \n AND trigger_kind = $4\n ",
"describe": {
"columns": [],
"parameters": {
@@ -21,7 +21,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -30,5 +31,5 @@
},
"nullable": []
},
"hash": "c5270ee815689e42b65df507b850da43239c9a5aaea41c9aed7ed33a6219a534"
"hash": "42b4b73e9d60348e2d90fcade9dcad6d8995242dc20a4e14c1a8fae4fc6a9fd2"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n capture_config \n SET \n error = $1, \n server_id = NULL, \n last_server_ping = NULL \n WHERE \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'gcp'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "43c8cd9f8560412bb06d9966ccfa2524943ba277c5f9a37c06bd5ee14fd46bda"
}
@@ -0,0 +1,36 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger \n SET \n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6, \n edited_by = $7, \n email = $8,\n script_path = $9,\n path = $10,\n enabled = $11,\n edited_at = now(), \n error = NULL,\n server_id = NULL\n WHERE \n workspace_id = $12 AND \n path = $13\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
},
"Jsonb",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "44b9bea3651edc8ee732def1241b3d956c004376102ccc1707fc016801599dbd"
}
@@ -220,7 +220,9 @@
"schedule",
"app",
"ui",
"postgres"
"postgres",
"sqs",
"gcp"
]
}
}
@@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n gcp_resource_path, \n script_path,\n is_flow, \n workspace_id,\n path,\n edited_by,\n email,\n delivery_config AS \"delivery_config: _\"\n FROM\n gcp_trigger\n WHERE\n workspace_id = $1 AND\n path = $2 AND\n delivery_type = 'push'::DELIVERY_MODE \n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "gcp_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "delivery_config: _",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "5303206bbed76ee3ddc56d8057d8b82359c182ee2a5da6df35a4375d5d2d1ef7"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE \n capture_config \n SET \n last_server_ping = NULL \n WHERE \n workspace_id = $1 AND \n path = $2 AND \n is_flow = $3 AND \n trigger_kind = 'gcp' AND \n server_id IS NULL\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "673564e6c4dcf30dae3d7a75c397998ea800860ede856e5fc6cd1f57d5408333"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload!: _\", trigger_extra as \"trigger_extra: _\" FROM capture WHERE id = $1 AND workspace_id = $2",
"query": "\n SELECT \n id, \n created_at, \n trigger_kind AS \"trigger_kind: _\", \n payload AS \"payload!: _\", \n trigger_extra AS \"trigger_extra: _\"\n FROM \n capture\n WHERE \n id = $1 \n AND workspace_id = $2\n ",
"describe": {
"columns": [
{
@@ -29,7 +29,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -60,5 +61,5 @@
true
]
},
"hash": "e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7"
"hash": "6781ba76dfce321dca4634566496ea5d698ac09d8264e35dfaa4cd8edc9e8414"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as \"payload!: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
"query": "\n SELECT \n id, \n created_at, \n trigger_kind AS \"trigger_kind: _\",\n CASE \n WHEN pg_column_size(payload) < 40000 THEN payload \n ELSE '\"WINDMILL_TOO_BIG\"'::jsonb \n END AS \"payload!: _\",\n trigger_extra AS \"trigger_extra: _\"\n FROM \n capture\n WHERE \n workspace_id = $1 \n AND path = $2 \n AND is_flow = $3 \n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY \n created_at DESC\n OFFSET $5\n LIMIT $6\n ",
"describe": {
"columns": [
{
@@ -29,7 +29,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -64,7 +65,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -81,5 +83,5 @@
true
]
},
"hash": "5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd"
"hash": "7cba31d597215a343cb0bca5b204a15fbba193262f7895c2bba90feb4215d6f3"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM \n capture\n WHERE \n id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "7d5a161d916cb8d1485f8d72e6b4044f505f49edcd2ebeee4a77eae737f48795"
}
@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE capture_config SET path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "7e891e053b8545c800d629421f239319a854f761a300970e2fd909f8058ec566"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT trigger_config as \"trigger_config: _\", trigger_kind as \"trigger_kind: _\", error, last_server_ping\n FROM capture_config\n WHERE workspace_id = $1 AND path = $2 AND is_flow = $3",
"query": "\n SELECT \n trigger_config AS \"trigger_config: _\", \n trigger_kind AS \"trigger_kind: _\", \n error, \n last_server_ping\n FROM \n capture_config\n WHERE \n workspace_id = $1 \n AND path = $2 \n AND is_flow = $3\n ",
"describe": {
"columns": [
{
@@ -24,7 +24,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -55,5 +56,5 @@
true
]
},
"hash": "c223f8b7fa4ef1aa06e1ba2a56d677774aa237508d5610714efd2e9b8b93c7b8"
"hash": "7fbf72d9059fcd77e4c1112fa4fa22e4276c1da653475628889ce17dc904fbaa"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT owner, email\n FROM capture_config\n WHERE workspace_id = $1 AND path = $2 AND is_flow = $3 AND trigger_kind = $4 AND last_client_ping > NOW() - INTERVAL '10 seconds'",
"query": "\n SELECT \n owner, \n email\n FROM \n capture_config\n WHERE \n workspace_id = $1 \n AND path = $2 \n AND is_flow = $3 \n AND trigger_kind = $4 \n AND last_client_ping > NOW() - INTERVAL '10 seconds'\n ",
"describe": {
"columns": [
{
@@ -32,7 +32,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -44,5 +45,5 @@
false
]
},
"hash": "71d51bbc35da7b9930e3ea3a634451217ccb9f1bc35b1ad6e10d16bc19c41447"
"hash": "87564a196a1662f524407d853db506bf08c28efe82b68b3d44bafbd3d0e91c29"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger \n SET \n server_id = $1, \n last_server_ping = now(),\n error = 'Connecting...'\n WHERE \n enabled IS TRUE \n AND workspace_id = $2 \n AND path = $3 \n AND (last_server_ping IS NULL \n OR last_server_ping < now() - INTERVAL '15 seconds'\n ) \n RETURNING true\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "92c2b66eb6287449f5f8cc9f8d1329f748006ab131489d2fcac21d32641bb633"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM capture\n WHERE workspace_id = $1\n AND created_at <=\n (\n SELECT created_at\n FROM capture\n WHERE workspace_id = $1\n ORDER BY created_at DESC\n OFFSET $2\n LIMIT 1\n )",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": []
},
"hash": "97942578df746c8c8103b403cfc4e44ef5a0f082bdde854900064325adc4dd77"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger \n SET\n last_server_ping = NULL \n WHERE \n workspace_id = $1 \n AND path = $2 \n AND server_id IS NULL",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "999edc6f54a9efb6dc6237992dad59f418ed6fc2a98ddb9bbc33dce5f029d904"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n capture_config \n SET \n last_server_ping = now(), \n error = $1 \n WHERE \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'gcp' AND \n server_id = $5 AND \n last_client_ping > NOW() - INTERVAL '10 seconds' \n RETURNING 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Bool",
"Text"
]
},
"nullable": [
null
]
},
"hash": "a7ffc5b983d365159ef379ec6a2ab0dc7217d1b3f86f362c213982984d2cf652"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\"\n ",
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\",\n EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS \"gcp_used!\"\n ",
"describe": {
"columns": [
{
@@ -37,6 +37,11 @@
"ordinal": 6,
"name": "sqs_used!",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "gcp_used!",
"type_info": "Bool"
}
],
"parameters": {
@@ -51,8 +56,9 @@
null,
null,
null,
null,
null
]
},
"hash": "9ae20f2f29406cfa5337736ad710c496f1757d950cdec63c93c77fe79d5212cc"
"hash": "aa0215d4174c1aeda8631bcd582c895329d2daf722d360fbcbdef6f04bb1400f"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO capture_config\n (workspace_id, path, is_flow, trigger_kind, trigger_config, owner, email)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (workspace_id, path, is_flow, trigger_kind)\n DO UPDATE SET trigger_config = $5, owner = $6, email = $7, server_id = NULL, error = NULL",
"query": "\n INSERT INTO capture_config (\n workspace_id, path, is_flow, trigger_kind, trigger_config, owner, email\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ON CONFLICT (workspace_id, path, is_flow, trigger_kind)\n DO UPDATE \n SET \n trigger_config = $5, \n owner = $6, \n email = $7, \n server_id = NULL, \n error = NULL\n ",
"describe": {
"columns": [],
"parameters": {
@@ -21,7 +21,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -33,5 +34,5 @@
},
"nullable": []
},
"hash": "62475252dcf54f32433b97ae011daf5d4205d160d2aedf463c7dfe944e93257a"
"hash": "b3f0595cacba194e08b9a3e244d9e637e9e156cd85b69126c87dfff89a47711d"
}
@@ -0,0 +1,50 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n path,\n is_flow,\n workspace_id,\n owner,\n email,\n trigger_config as \"trigger_config!: _\"\n FROM\n capture_config\n WHERE\n trigger_kind = 'gcp' AND\n last_client_ping > NOW() - INTERVAL '10 seconds' AND\n trigger_config IS NOT NULL AND\n trigger_config->>'delivery_type' IS DISTINCT FROM 'push' AND\n (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "owner",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "trigger_config!: _",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "c0e6dbce7a401b06e1bf45155c3f81572818177a6024e743b12f558b46edf74c"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE \n FROM \n gcp_trigger \n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c19a60a9dc3f95af218baf40c62f14572ac204cfe377166aa1d91cf58f731f50"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n capture_config\n SET \n path = $1\n WHERE \n path = $2 \n AND workspace_id = $3 \n AND is_flow = $4\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "c353aa9abe673749f0836fd16894d91ba89aab2318d85621ad2868017adfb48a"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927"
@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger\n SET \n last_server_ping = now(),\n error = $1\n WHERE\n workspace_id = $2\n AND path = $3\n AND server_id = $4 \n AND enabled IS TRUE\n RETURNING 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "df2a426658f0a36683cc2163fe70912d0221d191b8ab091601cb53cdd932ad58"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM \n capture\n WHERE \n workspace_id = $1\n AND created_at <= (\n SELECT \n created_at\n FROM \n capture\n WHERE \n workspace_id = $1\n ORDER BY \n created_at DESC\n OFFSET $2\n LIMIT 1\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": []
},
"hash": "e04a8a9f1e9cc3bb5c990194585e08f4c248e1af1c6580bdf8f2735ae1388981"
}
@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE capture SET path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "e70c3da5864b7946735b7ad6c416874ee8b277a2914dff6f9c33a1f5a2351114"
}
@@ -0,0 +1,129 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n subscription_id,\n topic_id,\n workspace_id,\n delivery_type AS \"delivery_type: _\",\n delivery_config AS \"delivery_config: _\",\n path,\n script_path,\n is_flow,\n edited_by,\n email,\n edited_at,\n server_id,\n last_server_ping,\n extra_perms,\n error,\n enabled\n FROM \n gcp_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "gcp_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "subscription_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "topic_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "delivery_type: _",
"type_info": {
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
}
},
{
"ordinal": 5,
"name": "delivery_config: _",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "is_flow",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "edited_at",
"type_info": "Timestamptz"
},
{
"ordinal": 12,
"name": "server_id",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "last_server_ping",
"type_info": "Timestamptz"
},
{
"ordinal": 14,
"name": "extra_perms",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 16,
"name": "enabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
false
]
},
"hash": "f4f6336fc671b00bed7835124892f7a4d3bbe673f7c48153819dab385a5cb357"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO capture (workspace_id, path, is_flow, trigger_kind, payload, trigger_extra, created_by)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
"query": "\n INSERT INTO \n capture (\n workspace_id, path, is_flow, trigger_kind, payload, trigger_extra, created_by\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -21,7 +21,8 @@
"nats",
"postgres",
"sqs",
"mqtt"
"mqtt",
"gcp"
]
}
}
@@ -33,5 +34,5 @@
},
"nullable": []
},
"hash": "07da723ce5c9ee2d7c236e8eabe254c783fc34b617c8a9a95a0eb0cda535dab5"
"hash": "f9f7f31390f8ea1f4facd8e6a888886ea136f2327169bf93f126e2ef130d0946"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE \n gcp_trigger \n SET \n enabled = FALSE, \n error = $1, \n server_id = NULL, \n last_server_ping = NULL \n WHERE \n workspace_id = $2 AND \n path = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "fd0a0ca4a107dc813240ad71f372ab7e0bf26431158fbfb5f0023ed847ca7dc5"
}
+1 -1
View File
@@ -11,5 +11,5 @@
"remote.autoForwardPorts": true,
"conventionalCommits.scopes": [
"restructring triggers, decoding trigger message on work"
]
],
}
+139 -4
View File
@@ -554,6 +554,17 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-compression"
version = "0.4.19"
@@ -4602,6 +4613,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "5.4.0"
@@ -5347,6 +5364,94 @@ dependencies = [
"gl_generator",
]
[[package]]
name = "google-cloud-auth"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57a13fbacc5e9c41ded3ad8d0373175a6b7a6ad430d99e89d314ac121b7ab06"
dependencies = [
"async-trait",
"base64 0.21.7",
"google-cloud-metadata",
"google-cloud-token",
"home",
"jsonwebtoken 9.3.1",
"reqwest 0.12.15",
"serde",
"serde_json",
"thiserror 1.0.69",
"time",
"tokio",
"tracing",
"urlencoding",
]
[[package]]
name = "google-cloud-gax"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de13e62d7e0ffc3eb40a0113ddf753cf6ec741be739164442b08893db4f9bfca"
dependencies = [
"google-cloud-token",
"http 1.3.1",
"thiserror 1.0.69",
"tokio",
"tokio-retry2",
"tonic",
"tower 0.4.13",
"tracing",
]
[[package]]
name = "google-cloud-googleapis"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "886aa8ec755382a1fdf4651f6e6ec01f2f3bf49f2cb0f068b9a74cafd574a715"
dependencies = [
"prost",
"prost-types",
"tonic",
]
[[package]]
name = "google-cloud-metadata"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d901aeb453fd80e51d64df4ee005014f6cf39f2d736dd64f7239c132d9d39a6a"
dependencies = [
"reqwest 0.12.15",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "google-cloud-pubsub"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebc6e7327e49a66ffb40508c673b7643191bd6b509530193bda97f09272cdcf"
dependencies = [
"async-channel",
"async-stream",
"google-cloud-auth",
"google-cloud-gax",
"google-cloud-googleapis",
"google-cloud-token",
"prost-types",
"thiserror 1.0.69",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "google-cloud-token"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c12ba8b21d128a2ce8585955246977fbce4415f680ebf9199b6f9d6d725f"
dependencies = [
"async-trait",
]
[[package]]
name = "gosyn"
version = "0.2.9"
@@ -6427,6 +6532,21 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.22.1",
"js-sys",
"pem 3.0.5",
"ring 0.17.14",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "junction"
version = "1.2.0"
@@ -11025,7 +11145,7 @@ dependencies = [
"crc",
"crossbeam-queue",
"either",
"event-listener",
"event-listener 5.4.0",
"futures-core",
"futures-intrusive",
"futures-io",
@@ -12447,6 +12567,16 @@ dependencies = [
"whoami",
]
[[package]]
name = "tokio-retry2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1264d076dd34560544a2799e40e457bd07c43d30f4a845686b031bcd8455c84f"
dependencies = [
"pin-project",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@@ -12624,6 +12754,7 @@ dependencies = [
"axum",
"base64 0.22.1",
"bytes",
"flate2",
"h2 0.4.8",
"http 1.3.1",
"http-body 1.0.1",
@@ -12644,6 +12775,7 @@ dependencies = [
"tower-layer",
"tower-service",
"tracing",
"webpki-roots",
]
[[package]]
@@ -13843,6 +13975,8 @@ dependencies = [
"datafusion",
"futures",
"git-version",
"google-cloud-googleapis",
"google-cloud-pubsub",
"half",
"hex",
"hf-hub",
@@ -13850,7 +13984,7 @@ dependencies = [
"http 1.3.1",
"hyper 1.6.0",
"itertools 0.14.0",
"jsonwebtoken",
"jsonwebtoken 8.3.0",
"lazy_static",
"magic-crypt",
"mail-parser",
@@ -13893,6 +14027,7 @@ dependencies = [
"tokio-tar",
"tokio-tungstenite",
"tokio-util",
"tonic",
"tower 0.5.2",
"tower-cookies",
"tower-http",
@@ -13982,7 +14117,7 @@ dependencies = [
"hyper 1.6.0",
"indexmap 2.8.0",
"itertools 0.14.0",
"jsonwebtoken",
"jsonwebtoken 8.3.0",
"lazy_static",
"magic-crypt",
"mail-send",
@@ -14350,7 +14485,7 @@ dependencies = [
"git-version",
"hex",
"itertools 0.14.0",
"jsonwebtoken",
"jsonwebtoken 8.3.0",
"lazy_static",
"mappable-rc",
"mysql_async",
+4 -1
View File
@@ -69,13 +69,14 @@ nats = ["windmill-api/nats"]
otel = ["windmill-common/otel", "windmill-worker/otel"]
dind = ["windmill-worker/dind"]
websocket = ["windmill-api/websocket"]
http_trigger = ["windmill-api/http_trigger"]
postgres_trigger = ["windmill-api/postgres_trigger"]
mqtt_trigger = ["windmill-api/mqtt_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger"]
gcp_trigger = ["windmill-api/gcp_trigger"]
smtp = ["windmill-api/smtp", "windmill-common/smtp"]
license = ["windmill-api/license"]
oauth2 = ["windmill-api/oauth2"]
http_trigger = ["windmill-api/http_trigger"]
zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
scoped_cache = ["windmill-common/scoped_cache"]
@@ -242,6 +243,8 @@ deno_runtime = { version = "0.198.0", features = ["transpile"] }
deno_telemetry = "0.12.0"
deno_error = "=0.5.5"
google-cloud-pubsub = "0.30.0"
google-cloud-googleapis = {version = "0.16.1", features = ["pubsub"]}
# TODO: remove once deno fixes the issue on their end
# https://github.com/denoland/deno/issues/28557
winapi = { version = "0.3.9", features = ["sysinfoapi"] }
+1 -1
View File
@@ -1 +1 @@
0706783f5ebc6fad0f26cb1c0ba52fe1deeeb168
85c37983ffb8f622458425c182613206625c6cee
@@ -0,0 +1,3 @@
-- Add down migration script here
DROP TABLE gcp_trigger;
DROP TYPE DELIVERY_MODE;
@@ -0,0 +1,80 @@
-- Add up migration script here
ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'gcp';
CREATE TYPE DELIVERY_MODE AS ENUM ('push', 'pull');
CREATE TABLE gcp_trigger (
gcp_resource_path VARCHAR(255) NOT NULL,
topic_id VARCHAR(255) NOT NULL CHECK (
CHAR_LENGTH(topic_id) BETWEEN 3 AND 255
),
subscription_id VARCHAR(255) NOT NULL CHECK (
CHAR_LENGTH(subscription_id) BETWEEN 3 AND 255
),
delivery_type DELIVERY_MODE NOT NULL,
delivery_config JSONB NULL CHECK (delivery_type != 'push'::DELIVERY_MODE OR (delivery_config IS NOT NULL)),
path VARCHAR(255) NOT NULL,
script_path VARCHAR(255) NOT NULL,
is_flow BOOLEAN NOT NULL,
workspace_id VARCHAR(50) NOT NULL,
edited_by VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
extra_perms JSONB NOT NULL DEFAULT '{}',
server_id VARCHAR(50),
last_server_ping TIMESTAMPTZ,
error TEXT,
enabled BOOLEAN NOT NULL,
PRIMARY KEY (path, workspace_id)
);
CREATE UNIQUE INDEX unique_subscription_per_gcp_resource
ON gcp_trigger (subscription_id, gcp_resource_path, workspace_id);
GRANT ALL ON gcp_trigger TO windmill_user;
GRANT ALL ON gcp_trigger TO windmill_admin;
ALTER TABLE gcp_trigger ENABLE ROW LEVEL SECURITY;
CREATE POLICY admin_policy ON gcp_trigger FOR ALL TO windmill_admin USING (true);
CREATE POLICY see_folder_extra_perms_user_select ON gcp_trigger FOR SELECT TO windmill_user
USING (SPLIT_PART(gcp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(gcp_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_insert ON gcp_trigger FOR INSERT TO windmill_user
WITH CHECK (SPLIT_PART(gcp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(gcp_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_update ON gcp_trigger FOR UPDATE TO windmill_user
USING (SPLIT_PART(gcp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(gcp_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_folder_extra_perms_user_delete ON gcp_trigger FOR DELETE TO windmill_user
USING (SPLIT_PART(gcp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(gcp_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
CREATE POLICY see_own ON gcp_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(gcp_trigger.path, '/', 1) = 'u' AND SPLIT_PART(gcp_trigger.path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member ON gcp_trigger FOR ALL TO windmill_user
USING (SPLIT_PART(gcp_trigger.path, '/', 1) = 'g' AND SPLIT_PART(gcp_trigger.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_extra_perms_user_select ON gcp_trigger FOR SELECT TO windmill_user
USING (extra_perms ? CONCAT('u/', current_setting('session.user')));
CREATE POLICY see_extra_perms_user_insert ON gcp_trigger FOR INSERT TO windmill_user
WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_update ON gcp_trigger FOR UPDATE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_user_delete ON gcp_trigger FOR DELETE TO windmill_user
USING ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean);
CREATE POLICY see_extra_perms_groups_select ON gcp_trigger FOR SELECT TO windmill_user
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]);
CREATE POLICY see_extra_perms_groups_insert ON gcp_trigger FOR INSERT TO windmill_user
WITH CHECK (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_update ON gcp_trigger FOR UPDATE TO windmill_user
USING (exists(
SELECT key, value FROM jsonb_each_text(extra_perms)
WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
AND value::boolean));
CREATE POLICY see_extra_perms_groups_delete ON gcp_trigger FOR DELETE TO windmill_user
USING (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 @@
-- Add down migration script here
@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'sqs';
ALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'gcp';
+3 -10
View File
@@ -13,7 +13,7 @@ use std::{
use chrono::{NaiveDateTime, Utc};
use futures::{stream::FuturesUnordered, StreamExt};
use serde::{de::DeserializeOwned, Deserializer};
use serde::de::DeserializeOwned;
use sqlx::{Pool, Postgres};
use tokio::{
join,
@@ -34,6 +34,7 @@ use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
use windmill_common::{
utils::empty_string_as_none,
agent_workers::DECODED_AGENT_TOKEN, auth::create_token_for_owner, ee::CriticalErrorChannel, error, flow_status::{FlowStatus, FlowStatusModule}, global_settings::{
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
CRITICAL_ERROR_CHANNELS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
@@ -46,7 +47,7 @@ use windmill_common::{
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
}, indexer::load_indexer_config, jobs::QueuedJob, jwt::JWT_SECRET, oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH, server::load_smtp_config, tracing_init::JSON_FMT, users::truncate_token, utils::{now_from_db, rd_string, report_critical_error, Mode}, worker::{
load_worker_config, reload_custom_tags_setting, store_pull_query, store_suspended_pull_query, update_min_version, Connection, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, TMP_DIR, WORKER_CONFIG, WORKER_GROUP
}, KillpillSender, BASE_URL, CRITICAL_ALERT_MUTE_UI_ENABLED, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS, METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED, OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS
}, KillpillSender, BASE_URL, CRITICAL_ALERT_MUTE_UI_ENABLED, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS, METRICS_DEBUG_ENABLED, METRICS_ENABLED, MONITOR_LOGS_ON_OBJECT_STORE, OTEL_LOGS_ENABLED, OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
};
use windmill_queue::{cancel_job, MiniPulledJob, SameWorkerPayload};
use windmill_worker::{
@@ -221,14 +222,6 @@ pub async fn load_metrics_enabled(conn: &Connection) -> error::Result<()> {
Ok(())
}
fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let option = <Option<String> as serde::Deserialize>::deserialize(deserializer)?;
Ok(option.filter(|s| !s.is_empty()))
}
#[derive(serde::Deserialize)]
struct OtelSetting {
metrics_enabled: Option<bool>,
+5 -1
View File
@@ -32,6 +32,7 @@ static_frontend = ["dep:rust-embed"]
postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal", "dep:rust-postgres-native-tls"]
mqtt_trigger = ["dep:thiserror", "dep:rumqttc"]
sqs_trigger = ["dep:aws-sdk-sqs", "dep:thiserror", "dep:aws-config"]
gcp_trigger = ["dep:thiserror", "dep:google-cloud-pubsub", "dep:google-cloud-googleapis", "dep:tonic"]
cloud = ["windmill-common/cloud"]
[dependencies]
@@ -129,4 +130,7 @@ rust_decimal = { workspace = true, optional = true }
rust-postgres-native-tls = { workspace = true, optional = true}
rumqttc = { workspace = true, optional = true }
aws-sdk-sqs = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true}
aws-config = { workspace = true, optional = true }
google-cloud-pubsub = { workspace = true, optional = true }
google-cloud-googleapis = { workspace = true , optional = true }
tonic = { workspace = true, optional = true }
+438 -1
View File
@@ -2702,6 +2702,8 @@ paths:
type: boolean
mqtt_used:
type: boolean
gcp_used:
type: boolean
sqs_used:
type: boolean
required:
@@ -2711,6 +2713,7 @@ paths:
- nats_used
- postgres_used
- mqtt_used
- gcp_used
- sqs_used
/w/{workspace}/users/list:
get:
@@ -9222,6 +9225,264 @@ paths:
schema:
type: string
/w/{workspace}/gcp_triggers/create:
post:
summary: create gcp trigger
operationId: createGcpTrigger
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: new gcp trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewGcpTrigger"
responses:
"201":
description: gcp trigger created
content:
text/plain:
schema:
type: string
/w/{workspace}/gcp_triggers/update/{path}:
post:
summary: update gcp trigger
operationId: updateGcpTrigger
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated trigger
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EditGcpTrigger"
responses:
"200":
description: gcp trigger updated
content:
text/plain:
schema:
type: string
/w/{workspace}/gcp_triggers/delete/{path}:
delete:
summary: delete gcp trigger
operationId: deleteGcpTrigger
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: gcp trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/gcp_triggers/get/{path}:
get:
summary: get gcp trigger
operationId: getGcpTrigger
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: gcp trigger deleted
content:
application/json:
schema:
$ref: "#/components/schemas/GcpTrigger"
/w/{workspace}/gcp_triggers/list:
get:
summary: list gcp triggers
operationId: listGcpTriggers
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
required: true
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: path
description: filter by path
in: query
schema:
type: string
- name: is_flow
in: query
schema:
type: boolean
- name: path_start
in: query
schema:
type: string
responses:
"200":
description: gcp trigger list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/GcpTrigger"
/w/{workspace}/gcp_triggers/exists/{path}:
get:
summary: does gcp trigger exists
operationId: existsGcpTrigger
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: gcp trigger exists
content:
application/json:
schema:
type: boolean
/w/{workspace}/gcp_triggers/setenabled/{path}:
post:
summary: set enabled gcp trigger
operationId: setGcpTriggerEnabled
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: updated gcp trigger enable
required: true
content:
application/json:
schema:
type: object
properties:
enabled:
type: boolean
required:
- enabled
responses:
"200":
description: gcp trigger enabled set
content:
text/plain:
schema:
type: string
/w/{workspace}/gcp_triggers/test:
post:
summary: test gcp connection
operationId: testGcpConnection
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: test gcp connection
required: true
content:
application/json:
schema:
type: object
properties:
connection:
type: object
required:
- connection
responses:
"200":
description: try to connect to a gcp broker
content:
text/plain:
schema:
type: string
/w/{workspace}/gcp_triggers/subscriptions/delete/{path}:
delete:
summary: delete gcp trigger
operationId: deleteGcpSubscription
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: args to delete subscription from google cloud
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeleteGcpSubscription"
responses:
"200":
description: gcp trigger deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/gcp_triggers/topics/list/{path}:
get:
summary: list all topics of google cloud service
operationId: listGoogleTopics
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: get all google topics
content:
application/json:
schema:
type: array
items:
type: string
/w/{workspace}/gcp_triggers/subscriptions/list/{path}:
post:
summary: list all subscription of a give topic from google cloud service
operationId: listAllTGoogleTopicSubscriptions
tags:
- gcp_trigger
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
requestBody:
description: args to get subscription's topic from google cloud
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/GetAllTopicSubscription"
responses:
"200":
description: get all google topic subscriptions name
content:
application/json:
schema:
type: array
items:
type: string
/w/{workspace}/postgres_triggers/is_valid_postgres_configuration/{path}:
get:
@@ -10563,6 +10824,7 @@ paths:
nats_trigger,
postgres_trigger,
mqtt_trigger,
gcp_trigger,
sqs_trigger
]
responses:
@@ -10606,6 +10868,7 @@ paths:
nats_trigger,
postgres_trigger,
mqtt_trigger,
gcp_trigger,
sqs_trigger
]
requestBody:
@@ -10660,6 +10923,7 @@ paths:
nats_trigger,
postgres_trigger,
mqtt_trigger,
gcp_trigger,
sqs_trigger
]
requestBody:
@@ -10712,6 +10976,10 @@ paths:
responses:
"200":
description: capture config set
content:
application/json:
schema:
type: object
/w/{workspace}/capture/ping_config/{trigger_kind}/{runnable_kind}/{path}:
post:
@@ -14190,6 +14458,8 @@ components:
type: number
mqtt_count:
type: number
gcp_count:
type: number
sqs_count:
type: number
@@ -14474,6 +14744,173 @@ components:
- subscribe_topics
- mqtt_resource_path
DeliveryType:
type: string
enum:
- push
- pull
PushConfig:
type: object
properties:
audience:
type: string
authenticate:
type: boolean
base_endpoint:
type: string
required:
- authenticate
- base_endpoint
GcpTrigger:
allOf:
- $ref: "#/components/schemas/TriggerExtraProperty"
type: object
properties:
gcp_resource_path:
type: string
topic_id:
type: string
subscription_id:
type: string
server_id:
type: string
delivery_type:
$ref: "#/components/schemas/DeliveryType"
delivery_config:
$ref: "#/components/schemas/PushConfig"
last_server_ping:
type: string
format: date-time
error:
type: string
enabled:
type: boolean
required:
- gcp_resource_path
- topic_id
- subscription_id
- enabled
- delivery_type
SubscriptionMode:
type: string
enum:
- existing
- create_update
description: "The mode of subscription. 'existing' means using an existing GCP subscription, while 'create_update' involves creating or updating a new subscription."
GcpExistingSubscription:
type: object
properties:
subscription_id:
type: string
base_endpoint:
type: string
required:
- subscription_id
- base_endpoint
GcpCreateUpdateSubscription:
type: object
properties:
subscription_id:
type: string
delivery_type:
$ref: "#/components/schemas/DeliveryType"
delivery_config:
$ref: "#/components/schemas/PushConfig"
required:
- delivery_type
GcpSubscriptionModeConfig:
type: object
properties:
subscription_mode:
$ref: "#/components/schemas/SubscriptionMode"
required:
- subscription_mode
allOf:
- oneOf:
- $ref: "#/components/schemas/GcpExistingSubscription"
- $ref: "#/components/schemas/GcpCreateUpdateSubscription"
description: |
"This is a union type representing the subscription mode.
- 'existing': Represents an existing GCP subscription, and should be accompanied by an 'ExistingGcpSubscription' object.
- 'create_update': Represents a new or updated GCP subscription, and should be accompanied by a 'CreateUpdateConfig' object."
NewGcpTrigger:
type: object
properties:
gcp_resource_path:
type: string
topic_id:
type: string
subscription_mode:
$ref: "#/components/schemas/GcpSubscriptionModeConfig"
path:
type: string
script_path:
type: string
is_flow:
type: boolean
enabled:
type: boolean
required:
- path
- script_path
- is_flow
- gcp_resource_path
- topic_id
- subscription_mode
EditGcpTrigger:
type: object
properties:
gcp_resource_path:
type: string
topic_id:
type: string
subscription_mode:
$ref: "#/components/schemas/GcpSubscriptionModeConfig"
path:
type: string
script_path:
type: string
is_flow:
type: boolean
enabled:
type: boolean
required:
- path
- script_path
- is_flow
- enabled
- mqtt_resource_path
- subscription_id
- delivery_type
- topic_id
- subscription_mode
GetAllTopicSubscription:
type: object
properties:
topic_id:
type: string
required:
- topic_id
DeleteGcpSubscription:
type: object
properties:
subscription_id:
type: string
required:
- subscription_id
SqsTrigger:
allOf:
- $ref: "#/components/schemas/TriggerExtraProperty"
@@ -15859,7 +16296,7 @@ components:
CaptureTriggerKind:
type: string
enum: [webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt]
enum: [webhook, http, websocket, kafka, email, nats, postgres, sqs, mqtt, gcp]
Capture:
type: object
-1
View File
@@ -509,7 +509,6 @@ where
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
+366 -97
View File
@@ -7,34 +7,47 @@
*/
#[cfg(feature = "http_trigger")]
use crate::http_triggers::{build_http_trigger_extra, HttpMethod};
use {
crate::{
args::try_from_request_body,
http_triggers::{build_http_trigger_extra, HttpMethod},
},
axum::response::{IntoResponse, Response},
std::collections::HashMap,
};
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
use crate::gcp_triggers_ee::{
manage_google_subscription, process_google_push_request, validate_jwt_token, SubscriptionMode,
};
#[cfg(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
))]
use {
axum::extract::Request, http::HeaderMap, serde::de::DeserializeOwned,
windmill_common::error::Error,
};
#[cfg(all(feature = "enterprise", feature = "kafka"))]
use crate::kafka_triggers_ee::KafkaTriggerConfigConnection;
#[cfg(feature = "mqtt_trigger")]
use crate::mqtt_triggers::{MqttClientVersion, MqttV3Config, MqttV5Config, SubscribeTopic};
#[cfg(all(feature = "enterprise", feature = "nats"))]
use crate::nats_triggers_ee::NatsTriggerConfigConnection;
#[cfg(feature = "postgres_trigger")]
use crate::postgres_triggers::{
create_logical_replication_slot_query, create_publication_query, drop_publication_query,
generate_random_string, get_database_connection, PublicationData,
use {
crate::postgres_triggers::{
create_logical_replication_slot_query, create_publication_query, drop_publication_query,
generate_random_string, get_database_connection, PublicationData,
},
itertools::Itertools,
pg_escape::quote_literal,
};
#[cfg(feature = "http_trigger")]
use axum::extract::Request;
#[cfg(feature = "http_trigger")]
use axum::response::IntoResponse;
#[cfg(feature = "http_trigger")]
use http::HeaderMap;
#[cfg(feature = "postgres_trigger")]
use itertools::Itertools;
#[cfg(feature = "postgres_trigger")]
use pg_escape::quote_literal;
#[cfg(feature = "http_trigger")]
use serde::de::DeserializeOwned;
#[cfg(feature = "http_trigger")]
use std::collections::HashMap;
#[cfg(feature = "http_trigger")]
use windmill_common::error::Error;
use crate::{
args::WebhookArgs,
@@ -42,23 +55,26 @@ use crate::{
users::fetch_api_authed,
utils::RunnableKind,
};
use axum::{
extract::{Extension, Path, Query},
routing::{delete, get, head, post},
Json, Router,
};
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use sqlx::types::Json as SqlxJson;
use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
use windmill_queue::TriggerKind;
use windmill_queue::{PushArgs, PushArgsOwned};
use windmill_queue::{PushArgs, PushArgsOwned, TriggerKind};
const KEEP_LAST: i64 = 20;
@@ -85,14 +101,26 @@ pub fn workspaced_unauthed_service() -> Router {
head(|| async {}).post(webhook_payload),
);
#[cfg(feature = "http_trigger")]
#[cfg(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
))]
{
router.route("/http/:runnable_kind/:path/*route_path", {
#[cfg(feature = "http_trigger")]
let router = router.route("/http/:runnable_kind/:path/*route_path", {
head(|| async {}).fallback(http_payload)
})
});
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
let router = router.route("/gcp/:runnable_kind/*path", post(gcp_payload));
router
}
#[cfg(not(feature = "http_trigger"))]
#[cfg(not(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
)))]
{
router
}
@@ -124,6 +152,15 @@ pub struct SqsTriggerConfig {
pub message_attributes: Option<Vec<String>>,
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct GcpTriggerConfig {
pub gcp_resource_path: String,
#[serde(flatten)]
pub subscription_mode: SubscriptionMode,
pub topic_id: String,
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
#[derive(Serialize, Deserialize)]
pub struct NatsTriggerConfig {
@@ -181,6 +218,8 @@ enum TriggerConfig {
Nats(NatsTriggerConfig),
#[cfg(feature = "mqtt_trigger")]
Mqtt(MqttTriggerConfig),
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
Gcp(GcpTriggerConfig),
}
#[derive(Serialize, Deserialize)]
@@ -208,15 +247,26 @@ async fn get_configs(
let configs = sqlx::query_as!(
CaptureConfig,
r#"SELECT trigger_config as "trigger_config: _", trigger_kind as "trigger_kind: _", error, last_server_ping
FROM capture_config
WHERE workspace_id = $1 AND path = $2 AND is_flow = $3"#,
r#"
SELECT
trigger_config AS "trigger_config: _",
trigger_kind AS "trigger_kind: _",
error,
last_server_ping
FROM
capture_config
WHERE
workspace_id = $1
AND path = $2
AND is_flow = $3
"#,
&w_id,
&path.to_path(),
matches!(runnable_kind, RunnableKind::Flow),
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(configs))
@@ -284,33 +334,100 @@ async fn set_postgres_trigger_config(
Ok(capture_config)
}
#[inline]
#[cfg(not(feature = "postgres_trigger"))]
async fn set_postgres_trigger_config(
_w_id: &str,
_authed: ApiAuthed,
_db: &DB,
_user_db: UserDB,
capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
Ok(capture_config)
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
async fn set_gcp_trigger_config(
w_id: &str,
authed: ApiAuthed,
db: &DB,
mut capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
let Some(TriggerConfig::Gcp(mut gcp_config)) = capture_config.trigger_config else {
return Err(windmill_common::error::Error::BadRequest(
"Invalid GCP Pub/Sub config".to_string(),
));
};
let config = manage_google_subscription(
authed,
db,
w_id,
&gcp_config.gcp_resource_path,
&capture_config.path,
&gcp_config.topic_id,
gcp_config.subscription_mode,
)
.await?;
gcp_config.subscription_mode = SubscriptionMode::CreateUpdate(config);
capture_config.trigger_config = Some(TriggerConfig::Gcp(gcp_config));
Ok(capture_config)
}
#[inline]
#[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))]
async fn set_gcp_trigger_config(
_w_id: &str,
_authed: ApiAuthed,
_db: &DB,
capture_config: NewCaptureConfig,
) -> Result<NewCaptureConfig> {
Ok(capture_config)
}
async fn set_config(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
#[cfg(feature = "postgres_trigger")] Extension(db): Extension<DB>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(nc): Json<NewCaptureConfig>,
) -> Result<()> {
#[cfg(feature = "postgres_trigger")]
let nc = if let TriggerKind::Postgres = nc.trigger_kind {
set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await?
} else {
nc
) -> JsonResult<Option<TriggerConfig>> {
let nc = match nc.trigger_kind {
TriggerKind::Postgres => {
set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await?
}
TriggerKind::Gcp => set_gcp_trigger_config(&w_id, authed.clone(), &db, nc).await?,
_ => nc,
};
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"INSERT INTO capture_config
(workspace_id, path, is_flow, trigger_kind, trigger_config, owner, email)
VALUES ($1, $2, $3, $4, $5, $6, $7)
r#"
INSERT INTO capture_config (
workspace_id, path, is_flow, trigger_kind, trigger_config, owner, email
)
VALUES (
$1, $2, $3, $4, $5, $6, $7
)
ON CONFLICT (workspace_id, path, is_flow, trigger_kind)
DO UPDATE SET trigger_config = $5, owner = $6, email = $7, server_id = NULL, error = NULL",
DO UPDATE
SET
trigger_config = $5,
owner = $6,
email = $7,
server_id = NULL,
error = NULL
"#,
&w_id,
&nc.path,
nc.is_flow,
nc.trigger_kind as TriggerKind,
nc.trigger_config.map(|x| SqlxJson(to_raw_value(&x))) as Option<SqlxJson<Box<RawValue>>>,
nc.trigger_config
.as_ref()
.map(|x| SqlxJson(to_raw_value(&x))) as Option<SqlxJson<Box<RawValue>>>,
&authed.username,
&authed.email,
)
@@ -319,7 +436,7 @@ async fn set_config(
tx.commit().await?;
Ok(())
Ok(Json(nc.trigger_config))
}
async fn ping_config(
@@ -333,8 +450,19 @@ async fn ping_config(
)>,
) -> Result<()> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"UPDATE capture_config SET last_client_ping = now() WHERE workspace_id = $1 AND path = $2 AND is_flow = $3 AND trigger_kind = $4",
r#"
UPDATE
capture_config
SET
last_client_ping = NOW()
WHERE
workspace_id = $1
AND path = $2
AND is_flow = $3
AND trigger_kind = $4
"#,
&w_id,
&path.to_path(),
matches!(runnable_kind, RunnableKind::Flow),
@@ -342,6 +470,7 @@ async fn ping_config(
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
@@ -374,14 +503,28 @@ async fn list_captures(
let captures = sqlx::query_as!(
Capture,
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '"WINDMILL_TOO_BIG"'::jsonb END as "payload!: _", trigger_extra as "trigger_extra: _"
FROM capture
WHERE workspace_id = $1
AND path = $2 AND is_flow = $3
r#"
SELECT
id,
created_at,
trigger_kind AS "trigger_kind: _",
CASE
WHEN pg_column_size(payload) < 40000 THEN payload
ELSE '"WINDMILL_TOO_BIG"'::jsonb
END AS "payload!: _",
trigger_extra AS "trigger_extra: _"
FROM
capture
WHERE
workspace_id = $1
AND path = $2
AND is_flow = $3
AND ($4::trigger_kind IS NULL OR trigger_kind = $4)
ORDER BY created_at DESC
ORDER BY
created_at DESC
OFFSET $5
LIMIT $6"#,
LIMIT $6
"#,
&w_id,
&path.to_path(),
matches!(runnable_kind, RunnableKind::Flow),
@@ -403,14 +546,28 @@ async fn get_capture(
Path((w_id, id)): Path<(String, i64)>,
) -> JsonResult<Capture> {
let mut tx = user_db.begin(&authed).await?;
let capture = sqlx::query_as!(
Capture,
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload!: _", trigger_extra as "trigger_extra: _" FROM capture WHERE id = $1 AND workspace_id = $2"#,
r#"
SELECT
id,
created_at,
trigger_kind AS "trigger_kind: _",
payload AS "payload!: _",
trigger_extra AS "trigger_extra: _"
FROM
capture
WHERE
id = $1
AND workspace_id = $2
"#,
id,
&w_id,
)
.fetch_one(&mut *tx)
.await?;
.await?;
tx.commit().await?;
Ok(Json(capture))
}
@@ -421,9 +578,17 @@ async fn delete_capture(
Path((_, id)): Path<(String, i64)>,
) -> Result<()> {
let mut tx = user_db.begin(&authed).await?;
sqlx::query!("DELETE FROM capture WHERE id = $1", id)
.execute(&mut *tx)
.await?;
sqlx::query!(
r#"
DELETE FROM
capture
WHERE
id = $1
"#,
id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
@@ -441,8 +606,18 @@ async fn move_captures_and_configs(
) -> Result<()> {
let mut tx = user_db.begin(&authed).await?;
let old_path = old_path.to_path();
sqlx::query!(
"UPDATE capture_config SET path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow = $4",
r#"
UPDATE
capture_config
SET
path = $1
WHERE
path = $2
AND workspace_id = $3
AND is_flow = $4
"#,
body.new_path,
old_path,
&w_id,
@@ -450,8 +625,18 @@ async fn move_captures_and_configs(
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"UPDATE capture SET path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow = $4",
r#"
UPDATE
capture
SET
path = $1
WHERE
path = $2
AND workspace_id = $3
AND is_flow = $4
"#,
body.new_path,
old_path,
&w_id,
@@ -459,6 +644,7 @@ async fn move_captures_and_configs(
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
@@ -478,9 +664,19 @@ pub async fn get_active_capture_owner_and_email(
) -> Result<(String, String)> {
let capture_config = sqlx::query_as!(
ActiveCaptureOwner,
"SELECT owner, email
FROM capture_config
WHERE workspace_id = $1 AND path = $2 AND is_flow = $3 AND trigger_kind = $4 AND last_client_ping > NOW() - INTERVAL '10 seconds'",
r#"
SELECT
owner,
email
FROM
capture_config
WHERE
workspace_id = $1
AND path = $2
AND is_flow = $3
AND trigger_kind = $4
AND last_client_ping > NOW() - INTERVAL '10 seconds'
"#,
&w_id,
&path,
is_flow,
@@ -498,7 +694,10 @@ pub async fn get_active_capture_owner_and_email(
Ok((capture_config.owner, capture_config.email))
}
#[cfg(feature = "http_trigger")]
#[cfg(any(
feature = "http_trigger",
all(feature = "enterprise", feature = "gcp_trigger")
))]
async fn get_capture_trigger_config_and_owner<T: DeserializeOwned>(
db: &DB,
w_id: &str,
@@ -512,16 +711,34 @@ async fn get_capture_trigger_config_and_owner<T: DeserializeOwned>(
owner: String,
email: String,
}
let capture_config = sqlx::query_as!(
CaptureTriggerConfigAndOwner,
r#"SELECT trigger_config as "trigger_config: _", owner, email
FROM capture_config
WHERE workspace_id = $1 AND path = $2 AND is_flow = $3 AND trigger_kind = $4 AND last_client_ping > NOW() - INTERVAL '10 seconds'"#,
r#"
SELECT
trigger_config AS "trigger_config: _",
owner,
email
FROM
capture_config
WHERE
workspace_id = $1
AND path = $2
AND is_flow = $3
AND trigger_kind = $4
AND last_client_ping > NOW() - INTERVAL '10 seconds'
AND (
$5::bool IS FALSE
OR (
trigger_config IS NOT NULL
AND trigger_config ->> 'delivery_type' = 'push'
)
)
"#,
&w_id,
&path,
is_flow,
kind as &TriggerKind,
matches!(kind, TriggerKind::Gcp)
)
.fetch_optional(db)
.await?;
@@ -554,17 +771,24 @@ async fn clear_captures_history(db: &DB, w_id: &str) -> Result<()> {
if *CLOUD_HOSTED {
/* Retain only KEEP_LAST most recent captures in this workspace. */
sqlx::query!(
"DELETE FROM capture
WHERE workspace_id = $1
AND created_at <=
(
SELECT created_at
FROM capture
WHERE workspace_id = $1
ORDER BY created_at DESC
OFFSET $2
LIMIT 1
)",
r#"
DELETE FROM
capture
WHERE
workspace_id = $1
AND created_at <= (
SELECT
created_at
FROM
capture
WHERE
workspace_id = $1
ORDER BY
created_at DESC
OFFSET $2
LIMIT 1
)
"#,
&w_id,
KEEP_LAST,
)
@@ -585,8 +809,15 @@ pub async fn insert_capture_payload(
owner: &str,
) -> Result<()> {
sqlx::query!(
"INSERT INTO capture (workspace_id, path, is_flow, trigger_kind, payload, trigger_extra, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)",
r#"
INSERT INTO
capture (
workspace_id, path, is_flow, trigger_kind, payload, trigger_extra, created_by
)
VALUES (
$1, $2, $3, $4, $5, $6, $7
)
"#,
&w_id,
path,
is_flow,
@@ -642,36 +873,74 @@ async fn webhook_payload(
Ok(StatusCode::NO_CONTENT)
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
async fn gcp_payload(
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, runnable_kind, path)): Path<(String, RunnableKind, String)>,
headers: HeaderMap,
request: Request,
) -> Result<StatusCode> {
let is_flow = matches!(runnable_kind, RunnableKind::Flow);
let (gcp_trigger_config, owner, email): (GcpTriggerConfig, _, _) =
get_capture_trigger_config_and_owner(&db, &w_id, &path, is_flow, &TriggerKind::Gcp).await?;
let authed = fetch_api_authed(owner.clone(), email, &w_id, &db, None).await?;
let SubscriptionMode::CreateUpdate(config) = &gcp_trigger_config.subscription_mode else {
return Err(Error::BadConfig("Bad config".to_string()));
};
validate_jwt_token(
&db,
user_db.clone(),
authed.clone(),
&headers,
&gcp_trigger_config.gcp_resource_path,
&w_id,
config.delivery_config.as_ref().unwrap(),
)
.await?;
let (args, extra) = process_google_push_request(headers, request).await?;
let payload = PushArgsOwned { args, extra: None };
let _ = insert_capture_payload(
&db,
&w_id,
&path,
is_flow,
&TriggerKind::Gcp,
payload,
Some(to_raw_value(&extra)),
&owner,
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[cfg(feature = "http_trigger")]
async fn http_payload(
Extension(db): Extension<DB>,
Path((w_id, kind, path, route_path)): Path<(String, RunnableKind, String, StripPath)>,
Path((w_id, runnable_kind, path, route_path)): Path<(String, RunnableKind, String, StripPath)>,
Query(query): Query<HashMap<String, String>>,
method: http::Method,
headers: HeaderMap,
request: Request,
) -> std::result::Result<StatusCode, impl IntoResponse> {
use axum::response::Response;
use crate::args::try_from_request_body;
let route_path = route_path.to_path();
) -> std::result::Result<StatusCode, Response> {
let path = path.replace(".", "/");
let is_flow = matches!(runnable_kind, RunnableKind::Flow);
let route_path = route_path.to_path();
let (http_trigger_config, owner, email): (HttpTriggerConfig, _, _) =
get_capture_trigger_config_and_owner(
&db,
&w_id,
&path,
matches!(kind, RunnableKind::Flow),
&TriggerKind::Http,
)
.await
.map_err(|e| e.into_response())?;
get_capture_trigger_config_and_owner(&db, &w_id, &path, is_flow, &TriggerKind::Http)
.await
.map_err(|e| e.into_response())?;
let args = try_from_request_body(
request,
&db,
&(),
http_trigger_config.raw_string,
http_trigger_config.wrap_body,
)
@@ -721,7 +990,7 @@ async fn http_payload(
&db,
&w_id,
&path,
matches!(kind, RunnableKind::Flow),
is_flow,
&TriggerKind::Http,
args,
extra,
@@ -730,5 +999,5 @@ async fn http_payload(
.await
.map_err(|e| e.into_response())?;
Ok::<_, Response>(StatusCode::NO_CONTENT)
Ok(StatusCode::NO_CONTENT)
}
+133
View File
@@ -0,0 +1,133 @@
use crate::db::{ApiAuthed, DB};
use axum::{extract::Request, Router};
use http::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use sqlx::prelude::FromRow;
use sqlx::types::Json as SqlxJson;
use std::collections::HashMap;
use windmill_common::db::UserDB;
use windmill_common::{
error::{Error as WindmillError, Result as WindmillResult},
utils::empty_string_as_none,
};
#[derive(sqlx::Type, Debug, Deserialize, Serialize)]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
#[sqlx(type_name = "DELIVERY_MODE", rename_all = "lowercase")]
#[allow(unused)]
pub enum DeliveryType {
Pull,
Push,
}
impl Default for DeliveryType {
fn default() -> Self {
Self::Pull
}
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
#[allow(unused)]
pub struct PushConfig {
#[serde(deserialize_with = "empty_string_as_none")]
route_path: Option<String>,
#[serde(deserialize_with = "empty_string_as_none")]
audience: Option<String>,
authenticate: bool,
base_endpoint: String,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct CreateUpdateConfig {
pub delivery_type: DeliveryType,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub subscription_id: Option<String>,
pub delivery_config: Option<SqlxJson<PushConfig>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ExistingGcpSubscription {
pub subscription_id: String,
pub base_endpoint: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "subscription_mode", rename_all = "snake_case")]
pub enum SubscriptionMode {
Existing(ExistingGcpSubscription),
CreateUpdate(CreateUpdateConfig),
}
pub fn workspaced_service() -> Router {
Router::new()
}
pub fn start_consuming_gcp_pubsub_event(
_db: DB,
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> () {
// implementation is not open source
}
pub async fn manage_google_subscription(
_authed: ApiAuthed,
_db: &DB,
_workspace_id: &str,
_gcp_resource_path: &str,
_path: &str,
_topic_id: &str,
_subscription_mode: SubscriptionMode,
) -> WindmillResult<CreateUpdateConfig> {
Ok(CreateUpdateConfig::default())
}
pub async fn process_google_push_request(
_headers: HeaderMap,
_request: Request,
) -> Result<
(
HashMap<String, Box<RawValue>>,
Option<HashMap<String, Box<RawValue>>>,
),
WindmillError,
> {
Ok((HashMap::new(), None))
}
pub async fn validate_jwt_token(
_db: &DB,
_user_db: UserDB,
_authed: ApiAuthed,
_headers: &HeaderMap,
_gcp_resource_path: &str,
_workspace_id: &str,
_delivery_config: &PushConfig,
) -> Result<(), windmill_common::error::Error> {
Ok(())
}
pub fn gcp_push_route_handler() -> Router {
Router::new()
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct GcpTrigger {
pub gcp_resource_path: String,
pub subscription_id: String,
pub delivery_type: DeliveryType,
pub delivery_config: Option<SqlxJson<PushConfig>>,
pub topic_id: String,
pub path: String,
pub script_path: String,
pub is_flow: bool,
pub workspace_id: String,
pub edited_by: String,
pub email: String,
pub edited_at: chrono::DateTime<chrono::Utc>,
pub extra_perms: Option<serde_json::Value>,
pub error: Option<String>,
pub server_id: Option<String>,
pub last_server_ping: Option<chrono::DateTime<chrono::Utc>>,
pub enabled: bool,
}
@@ -618,7 +618,9 @@ impl AuthenticationMethod {
api_key_header,
api_key_secret,
}) => {
let api_key_to_cmp = headers.try_get_webhook_header(&api_key_header)?;
let api_key_to_cmp = headers
.try_get_webhook_header(&api_key_header)
.map_err(|_| AuthenticationError::InvalidApiKey)?;
if api_key_to_cmp != api_key_secret {
return Err(AuthenticationError::InvalidApiKey);
}
@@ -734,7 +736,9 @@ impl IntoResponse for AuthenticationError {
)
.into_response()
}
AuthenticationError::InvalidApiKey => (StatusCode::FORBIDDEN, self.to_string()),
AuthenticationError::InvalidApiKey => {
return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
}
};
let body = json!({ "error": error_message });
+1 -1
View File
@@ -953,7 +953,7 @@ async fn route_job(
let args = try_from_request_body(
request,
&db,
&(),
Some(match trigger.authentication_method {
AuthenticationMethod::CustomScript | AuthenticationMethod::Signature => true,
_ => trigger.raw_string,
+36
View File
@@ -89,6 +89,8 @@ mod postgres_triggers;
#[cfg(feature = "enterprise")]
mod apps_ee;
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
mod gcp_triggers_ee;
#[cfg(feature = "enterprise")]
mod git_sync_ee;
#[cfg(feature = "parquet")]
@@ -117,6 +119,7 @@ mod slack_approvals;
mod smtp_server_ee;
#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))]
mod sqs_triggers_ee;
mod static_assets;
#[cfg(all(feature = "stripe", feature = "enterprise"))]
mod stripe_ee;
@@ -347,6 +350,18 @@ pub async fn run_server(
}
};
let gcp_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
{
gcp_triggers_ee::workspaced_service()
}
#[cfg(not(all(feature = "enterprise", feature = "gcp_trigger")))]
{
Router::new()
}
};
let sqs_triggers_service = {
#[cfg(all(feature = "enterprise", feature = "sqs_trigger"))]
{
@@ -425,6 +440,12 @@ pub async fn run_server(
let sqs_killpill_rx = killpill_rx.resubscribe();
sqs_triggers_ee::start_sqs(db.clone(), sqs_killpill_rx);
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
{
let gcp_killpill_rx = killpill_rx.resubscribe();
gcp_triggers_ee::start_consuming_gcp_pubsub_event(db.clone(), gcp_killpill_rx);
}
}
#[cfg(feature = "agent_worker_server")]
@@ -488,6 +509,7 @@ pub async fn run_server(
.nest("/nats_triggers", nats_triggers_service)
.nest("/mqtt_triggers", mqtt_triggers_service)
.nest("/sqs_triggers", sqs_triggers_service)
.nest("/gcp_triggers", gcp_triggers_service)
.nest("/postgres_triggers", postgres_triggers_service),
)
.nest("/workspaces", workspaces::global_service())
@@ -638,6 +660,20 @@ pub async fn run_server(
}
.layer(from_extractor::<OptAuthed>()),
)
.nest(
"/gcp/w/:workspace_id",
{
#[cfg(feature = "gcp_trigger")]
{
gcp_triggers_ee::gcp_push_route_handler()
}
#[cfg(not(feature = "gcp_trigger"))]
{
Router::new()
}
}
.layer(from_extractor::<OptAuthed>()),
)
.route("/version", get(git_v))
.route("/uptodate", get(is_up_to_date))
.route("/ee_license", get(ee_license))
+7 -5
View File
@@ -599,11 +599,10 @@ pub async fn transform_json_value<'c>(
}
Value::Object(mut m) => {
for (a, b) in m.clone().into_iter() {
m.insert(
a.clone(),
let v =
transform_json_value(authed, user_db.clone(), db, workspace, b, job_id, token)
.await?,
);
.await?;
m.insert(a.clone(), v);
}
Ok(Value::Object(m))
}
@@ -1212,7 +1211,10 @@ async fn update_resource_type(
feature = "http_trigger",
feature = "postgres_trigger",
feature = "mqtt_trigger",
all(feature = "sqs_trigger", feature = "enterprise")
all(
feature = "enterprise",
any(feature = "sqs_trigger", feature = "gcp_trigger")
)
))]
pub async fn try_get_resource_from_db_as<T>(
authed: ApiAuthed,
+3 -1
View File
@@ -1307,6 +1307,7 @@ struct UsedTriggers {
pub postgres_used: bool,
pub mqtt_used: bool,
pub sqs_used: bool,
pub gcp_used: bool
}
async fn get_used_triggers(
@@ -1325,7 +1326,8 @@ async fn get_used_triggers(
EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as "nats_used!",
EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS "postgres_used!",
EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS "mqtt_used!",
EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS "sqs_used!"
EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS "sqs_used!",
EXISTS(SELECT 1 FROM gcp_trigger WHERE workspace_id = $1) AS "gcp_used!"
"#,
w_id
)
@@ -637,6 +637,47 @@ pub(crate) async fn tarball_workspace(
}
}
#[cfg(all(feature = "enterprise", feature = "gcp_trigger"))]
{
let gcp_triggers = sqlx::query_as!(
crate::gcp_triggers_ee::GcpTrigger,
r#"
SELECT
gcp_resource_path,
subscription_id,
topic_id,
workspace_id,
delivery_type AS "delivery_type: _",
delivery_config AS "delivery_config: _",
path,
script_path,
is_flow,
edited_by,
email,
edited_at,
server_id,
last_server_ping,
extra_perms,
error,
enabled
FROM
gcp_trigger
WHERE
workspace_id = $1
"#,
&w_id
)
.fetch_all(&mut *tx)
.await?;
for trigger in gcp_triggers {
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
archive
.write_to_archive(&trigger_str, &format!("{}.gcp_trigger.json", trigger.path))
.await?;
}
}
#[cfg(all(feature = "enterprise", feature = "nats"))]
{
let nats_triggers = sqlx::query_as!(
+6
View File
@@ -22,6 +22,8 @@ pub type JsonResult<T> = std::result::Result<Json<T>, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("Bad gateway: {0}")]
BadGateway(String),
#[error("Bad config: {0}")]
BadConfig(String),
#[error("Connecting to database: {0}")]
@@ -76,6 +78,8 @@ pub enum Error {
FindPythonError(String),
#[error("Problem with arguments: {0}")]
ArgumentErr(String),
#[error("{1}")]
Generic(StatusCode, String),
}
fn prettify_location(location: &'static Location<'static>) -> String {
@@ -181,6 +185,8 @@ impl IntoResponse for Error {
| Self::BadRequest(_)
| Self::AIError(_)
| Self::QuotaExceeded(_) => axum::http::StatusCode::BAD_REQUEST,
Self::BadGateway(_) => axum::http::StatusCode::BAD_GATEWAY,
Self::Generic(status_code, _) => status_code,
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
};
+10 -10
View File
@@ -418,16 +418,6 @@ pub async fn report_critical_error(
}
}
pub fn empty_string_as_none<'de, D>(
deserializer: D,
) -> std::result::Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let option = <Option<String> as serde::Deserialize>::deserialize(deserializer)?;
Ok(option.filter(|s| !s.is_empty()))
}
pub async fn report_recovered_critical_error(
message: String,
_db: DB,
@@ -478,6 +468,16 @@ pub async fn report_recovered_critical_error(
}
}
pub fn empty_string_as_none<'de, D>(
deserializer: D,
) -> std::result::Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let option = <Option<String> as serde::Deserialize>::deserialize(deserializer)?;
Ok(option.filter(|s| !s.is_empty()))
}
pub async fn fetch_mute_workspace(_db: &DB, workspace_id: &str) -> Result<bool> {
match sqlx::query!(
"SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1",
+3
View File
@@ -1952,6 +1952,7 @@ pub enum TriggerKind {
Mqtt,
Sqs,
Postgres,
Gcp
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone)]
@@ -1968,6 +1969,7 @@ pub enum JobTriggerKind {
Sqs,
Postgres,
Schedule,
Gcp
}
impl fmt::Display for TriggerKind {
@@ -1982,6 +1984,7 @@ impl fmt::Display for TriggerKind {
TriggerKind::Mqtt => "mqtt",
TriggerKind::Sqs => "sqs",
TriggerKind::Postgres => "postgres",
TriggerKind::Gcp => "gcp",
};
write!(f, "{}", s)
}
+1
View File
@@ -33,6 +33,7 @@ use windmill_common::{
use anyhow::{anyhow, bail, Result};
use windmill_queue::MiniPulledJob;
use std::ops::AsyncFn;
use std::path::Path;
use std::{collections::HashMap, sync::Arc, time::Duration};
+5 -11
View File
@@ -8,9 +8,11 @@ use tiberius::{AuthMethod, Client, ColumnData, Config, FromSqlOwned, Query, Row,
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
use uuid::Uuid;
use windmill_common::error::to_anyhow;
use windmill_common::error::{self, Error};
use windmill_common::worker::{to_raw_value, Connection};
use windmill_common::{
error::{self, to_anyhow, Error},
utils::empty_string_as_none,
worker::{to_raw_value, Connection},
};
use windmill_parser_sql::{parse_db_resource, parse_mssql_sig};
use windmill_queue::MiniPulledJob;
use windmill_queue::{append_logs, CanceledBy};
@@ -379,14 +381,6 @@ fn sql_to_json_value(val: ColumnData) -> Result<Value, Error> {
}
}
fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let option = <Option<String> as serde::Deserialize>::deserialize(deserializer)?;
Ok(option.filter(|s| !s.is_empty()))
}
fn deserialize_aad_token<'de, D>(deserializer: D) -> Result<Option<AadToken>, D::Error>
where
D: Deserializer<'de>,
+1 -1
View File
@@ -54,7 +54,7 @@ export const OpenAPI: OpenAPIConfig = {
PASSWORD: undefined,
TOKEN: getEnv("WM_TOKEN"),
USERNAME: undefined,
VERSION: '1.479.2',
VERSION: '1.481.0',
WITH_CREDENTIALS: true,
interceptors: {
request: new Interceptors(),
+222 -1
View File
File diff suppressed because one or more lines are too long
+227 -6
View File
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AIProvider = 'openai' | 'anthropic' | 'mistral' | 'deepseek' | 'googleai' | 'groq' | 'openrouter' | 'togetherai' | 'customai';
export type AIProvider = 'openai' | 'azure_openai' | 'anthropic' | 'mistral' | 'deepseek' | 'googleai' | 'groq' | 'openrouter' | 'togetherai' | 'customai';
export type AIProviderModel = {
model: string;
@@ -659,6 +659,7 @@ export type TriggersCount = {
kafka_count?: number;
nats_count?: number;
mqtt_count?: number;
gcp_count?: number;
sqs_count?: number;
};
@@ -774,6 +775,80 @@ export type EditMqttTrigger = {
enabled: boolean;
};
export type DeliveryType = 'push' | 'pull';
export type PushConfig = {
audience?: string;
authenticate: boolean;
base_endpoint: string;
};
export type GcpTrigger = TriggerExtraProperty & {
gcp_resource_path: string;
topic_id: string;
subscription_id: string;
server_id?: string;
delivery_type: DeliveryType;
delivery_config?: PushConfig;
last_server_ping?: string;
error?: string;
enabled: boolean;
};
/**
* The mode of subscription. 'existing' means using an existing GCP subscription, while 'create_update' involves creating or updating a new subscription.
*/
export type SubscriptionMode = 'existing' | 'create_update';
export type GcpExistingSubscription = {
subscription_id: string;
base_endpoint: string;
};
export type GcpCreateUpdateSubscription = {
subscription_id?: string;
delivery_type: DeliveryType;
delivery_config?: PushConfig;
};
/**
* "This is a union type representing the subscription mode.
* - 'existing': Represents an existing GCP subscription, and should be accompanied by an 'ExistingGcpSubscription' object.
* - 'create_update': Represents a new or updated GCP subscription, and should be accompanied by a 'CreateUpdateConfig' object."
*
*/
export type GcpSubscriptionModeConfig = (GcpExistingSubscription | GcpCreateUpdateSubscription) & {
subscription_mode: SubscriptionMode;
};
export type NewGcpTrigger = {
gcp_resource_path: string;
topic_id: string;
subscription_mode: GcpSubscriptionModeConfig;
path: string;
script_path: string;
is_flow: boolean;
enabled?: boolean;
};
export type EditGcpTrigger = {
gcp_resource_path?: string;
topic_id: string;
subscription_mode: GcpSubscriptionModeConfig;
path: string;
script_path: string;
is_flow: boolean;
enabled: boolean;
};
export type GetAllTopicSubscription = {
topic_id: string;
};
export type DeleteGcpSubscription = {
subscription_id: string;
};
export type SqsTrigger = TriggerExtraProperty & {
queue_url: string;
aws_resource_path: string;
@@ -1384,7 +1459,7 @@ export type CriticalAlert = {
workspace_id?: (string) | null;
};
export type CaptureTriggerKind = 'webhook' | 'http' | 'websocket' | 'kafka' | 'email' | 'nats' | 'postgres' | 'sqs' | 'mqtt';
export type CaptureTriggerKind = 'webhook' | 'http' | 'websocket' | 'kafka' | 'email' | 'nats' | 'postgres' | 'sqs' | 'mqtt' | 'gcp';
export type Capture = {
trigger_kind: CaptureTriggerKind;
@@ -1889,6 +1964,11 @@ export type ParameterSuspended = boolean;
*/
export type ParameterRunning = boolean;
/**
* allow wildcards (*) in the filter of label, tag, worker
*/
export type ParameterAllowWildcards = boolean;
/**
* filter on jobs containing those args as a json subset (@> in postgres)
*/
@@ -2976,6 +3056,7 @@ export type GetUsedTriggersResponse = ({
nats_used: boolean;
postgres_used: boolean;
mqtt_used: boolean;
gcp_used: boolean;
sqs_used: boolean;
});
@@ -4983,6 +5064,10 @@ export type RunFlowPreviewData = {
export type RunFlowPreviewResponse = (string);
export type ListQueueData = {
/**
* allow wildcards (*) in the filter of label, tag, worker
*/
allowWildcards?: boolean;
/**
* get jobs from all workspaces (only valid if request come from the `admins` workspace)
*/
@@ -5108,6 +5193,10 @@ export type CountCompletedJobsData = {
export type CountCompletedJobsResponse = (number);
export type ListFilteredUuidsData = {
/**
* allow wildcards (*) in the filter of label, tag, worker
*/
allowWildcards?: boolean;
/**
* get jobs from all workspaces (only valid if request come from the `admins` workspace)
*/
@@ -5209,6 +5298,10 @@ export type CancelSelectionData = {
export type CancelSelectionResponse = (Array<(string)>);
export type ListCompletedJobsData = {
/**
* allow wildcards (*) in the filter of label, tag, worker
*/
allowWildcards?: boolean;
/**
* filter on jobs containing those args as a json subset (@> in postgres)
*/
@@ -5303,6 +5396,10 @@ export type ListCompletedJobsData = {
export type ListCompletedJobsResponse = (Array<CompletedJob>);
export type ListJobsData = {
/**
* allow wildcards (*) in the filter of label, tag, worker
*/
allowWildcards?: boolean;
/**
* get jobs from all workspaces (only valid if request come from the `admins` workspace)
*/
@@ -6366,6 +6463,124 @@ export type TestMqttConnectionData = {
export type TestMqttConnectionResponse = (string);
export type CreateGcpTriggerData = {
/**
* new gcp trigger
*/
requestBody: NewGcpTrigger;
workspace: string;
};
export type CreateGcpTriggerResponse = (string);
export type UpdateGcpTriggerData = {
path: string;
/**
* updated trigger
*/
requestBody: EditGcpTrigger;
workspace: string;
};
export type UpdateGcpTriggerResponse = (string);
export type DeleteGcpTriggerData = {
path: string;
workspace: string;
};
export type DeleteGcpTriggerResponse = (string);
export type GetGcpTriggerData = {
path: string;
workspace: string;
};
export type GetGcpTriggerResponse = (GcpTrigger);
export type ListGcpTriggersData = {
isFlow?: boolean;
/**
* which page to return (start at 1, default 1)
*/
page?: number;
/**
* filter by path
*/
path?: string;
pathStart?: string;
/**
* number of items to return for a given page (default 30, max 100)
*/
perPage?: number;
workspace: string;
};
export type ListGcpTriggersResponse = (Array<GcpTrigger>);
export type ExistsGcpTriggerData = {
path: string;
workspace: string;
};
export type ExistsGcpTriggerResponse = (boolean);
export type SetGcpTriggerEnabledData = {
path: string;
/**
* updated gcp trigger enable
*/
requestBody: {
enabled: boolean;
};
workspace: string;
};
export type SetGcpTriggerEnabledResponse = (string);
export type TestGcpConnectionData = {
/**
* test gcp connection
*/
requestBody: {
connection: {
[key: string]: unknown;
};
};
workspace: string;
};
export type TestGcpConnectionResponse = (string);
export type DeleteGcpSubscriptionData = {
path: string;
/**
* args to delete subscription from google cloud
*/
requestBody: DeleteGcpSubscription;
workspace: string;
};
export type DeleteGcpSubscriptionResponse = (string);
export type ListGoogleTopicsData = {
path: string;
workspace: string;
};
export type ListGoogleTopicsResponse = (Array<(string)>);
export type ListAllTgoogleTopicSubscriptionsData = {
path: string;
/**
* args to get subscription's topic from google cloud
*/
requestBody: GetAllTopicSubscription;
workspace: string;
};
export type ListAllTgoogleTopicSubscriptionsResponse = (Array<(string)>);
export type IsValidPostgresConfigurationData = {
path: string;
workspace: string;
@@ -6907,7 +7122,7 @@ export type ListAutoscalingEventsData = {
export type ListAutoscalingEventsResponse = (Array<AutoscalingEvent>);
export type GetGranularAclsData = {
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'sqs_trigger';
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'gcp_trigger' | 'sqs_trigger';
path: string;
workspace: string;
};
@@ -6917,7 +7132,7 @@ export type GetGranularAclsResponse = ({
});
export type AddGranularAclsData = {
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'sqs_trigger';
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'gcp_trigger' | 'sqs_trigger';
path: string;
/**
* acl to add
@@ -6932,7 +7147,7 @@ export type AddGranularAclsData = {
export type AddGranularAclsResponse = (string);
export type RemoveGranularAclsData = {
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'sqs_trigger';
kind: 'script' | 'group_' | 'resource' | 'schedule' | 'variable' | 'flow' | 'folder' | 'app' | 'raw_app' | 'http_trigger' | 'websocket_trigger' | 'kafka_trigger' | 'nats_trigger' | 'postgres_trigger' | 'mqtt_trigger' | 'gcp_trigger' | 'sqs_trigger';
path: string;
/**
* acl to add
@@ -6960,7 +7175,9 @@ export type SetCaptureConfigData = {
workspace: string;
};
export type SetCaptureConfigResponse = (unknown);
export type SetCaptureConfigResponse = ({
[key: string]: unknown;
});
export type PingCaptureConfigData = {
path: string;
@@ -7432,6 +7649,10 @@ export type GetConcurrencyKeyData = {
export type GetConcurrencyKeyResponse = (string);
export type ListExtendedJobsData = {
/**
* allow wildcards (*) in the filter of label, tag, worker
*/
allowWildcards?: boolean;
/**
* get jobs from all workspaces (only valid if request come from the `admins` workspace)
*/
+11 -2
View File
@@ -659,7 +659,9 @@ export async function elementsToMap(
path.endsWith(".nats_trigger" + ext) ||
path.endsWith(".postgres_trigger" + ext) ||
path.endsWith(".mqtt_trigger" + ext) ||
path.endsWith(".sqs_trigger" + ext))
path.endsWith(".sqs_trigger" + ext) ||
path.endsWith(".gcp_trigger" + ext)
)
)
continue;
if (!skips.includeUsers && path.endsWith(".user" + ext)) continue;
@@ -921,7 +923,8 @@ function getOrderFromPath(p: string) {
typ == "nats_trigger" ||
typ == "postgres_trigger" ||
typ == "mqtt_trigger" ||
typ == "sqs_trigger"
typ == "sqs_trigger" ||
typ == "gcp_trigger"
) {
return 8;
} else if (typ == "variable") {
@@ -1787,6 +1790,12 @@ export async function push(opts: GlobalOptions & SyncOptions) {
path: removeSuffix(target, ".sqs_trigger.json"),
});
break;
case "gcp_trigger":
await wmill.deleteGcpTrigger({
workspace: workspaceId,
path: removeSuffix(target, ".gcp_trigger.json"),
});
break;
case "variable":
await wmill.deleteVariable({
workspace: workspaceId,
+10 -2
View File
@@ -1,5 +1,6 @@
import * as wmill from "./gen/services.gen.ts";
import {
GcpTrigger,
HttpTrigger,
KafkaTrigger,
MqttTrigger,
@@ -27,6 +28,7 @@ type Trigger = {
postgres: PostgresTrigger;
mqtt: MqttTrigger;
sqs: SqsTrigger;
gcp: GcpTrigger
};
type TriggerFile<K extends TriggerType> = Omit<
@@ -60,6 +62,7 @@ async function getTrigger<K extends TriggerType>(
postgres: wmill.getPostgresTrigger,
mqtt: wmill.getMqttTrigger,
sqs: wmill.getSqsTrigger,
gcp: wmill.getGcpTrigger
};
const triggerFunction = triggerFunctions[triggerType];
@@ -87,6 +90,7 @@ async function updateTrigger<K extends TriggerType>(
postgres: wmill.updatePostgresTrigger,
mqtt: wmill.updateMqttTrigger,
sqs: wmill.updateSqsTrigger,
gcp: wmill.updateGcpTrigger
};
const triggerFunction = triggerFunctions[triggerType];
await triggerFunction({ workspace, path, requestBody: trigger });
@@ -112,6 +116,7 @@ async function createTrigger<K extends TriggerType>(
postgres: wmill.createPostgresTrigger,
mqtt: wmill.createMqttTrigger,
sqs: wmill.createSqsTrigger,
gcp: wmill.createGcpTrigger
};
const triggerFunction = triggerFunctions[triggerType];
await triggerFunction({ workspace, path, requestBody: trigger });
@@ -191,7 +196,9 @@ async function list(opts: GlobalOptions) {
const sqsTriggers = await wmill.listSqsTriggers({
workspace: workspace.workspaceId,
});
const gcpTriggers = await wmill.listGcpTriggers({
workspace: workspace.workspaceId
})
const triggers = [
...httpTriggers.map((x) => ({ path: x.path, kind: "http" })),
...websocketTriggers.map((x) => ({ path: x.path, kind: "websocket" })),
@@ -200,6 +207,7 @@ async function list(opts: GlobalOptions) {
...postgresTriggers.map((x) => ({ path: x.path, kind: "postgres" })),
...mqttTriggers.map((x) => ({ path: x.path, kind: "mqtt" })),
...sqsTriggers.map((x) => ({ path: x.path, kind: "sqs" })),
...gcpTriggers.map((x) => ({ path: x.path, kind: "gcp" }))
];
new Table()
@@ -213,7 +221,7 @@ async function list(opts: GlobalOptions) {
function checkIfValidTrigger(kind: string | undefined): kind is TriggerType {
if (
kind &&
["http", "websocket", "kafka", "nats", "postgres", "mqtt", "sqs"].includes(kind)
["http", "websocket", "kafka", "nats", "postgres", "mqtt", "sqs", "gcp"].includes(kind)
) {
return true;
} else {
+4
View File
@@ -154,6 +154,8 @@ export async function pushObj(
await pushTrigger("mqtt", workspace, p, befObj, newObj);
} else if (typeEnding === "sqs_trigger") {
await pushTrigger("sqs", workspace, p, befObj, newObj);
} else if (typeEnding === "gcp_trigger") {
await pushTrigger("gcp", workspace, p, befObj, newObj);
} else if (typeEnding === "user") {
await pushWorkspaceUser(workspace, p, befObj, newObj);
} else if (typeEnding === "group") {
@@ -203,6 +205,7 @@ export function getTypeStrFromPath(
| "postgres_trigger"
| "mqtt_trigger"
| "sqs_trigger"
| "gcp_trigger"
| "user"
| "group"
| "settings"
@@ -253,6 +256,7 @@ export function getTypeStrFromPath(
typeEnding === "postgres_trigger" ||
typeEnding === "mqtt_trigger" ||
typeEnding === "sqs_trigger" ||
typeEnding === "gcp_trigger" ||
typeEnding === "user" ||
typeEnding === "group" ||
typeEnding === "settings" ||
@@ -230,7 +230,8 @@
'routes',
'schedules',
'sqs',
'websockets'
'websockets',
'gcp'
]
if (
additionalInformation?.triggers &&
+9 -1
View File
@@ -20,7 +20,9 @@
PostgresTriggerService,
NatsTriggerService,
MqttTriggerService,
SqsTriggerService
SqsTriggerService,
GcpTriggerService
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher, getContext } from 'svelte'
@@ -49,6 +51,7 @@
| 'nats_trigger'
| 'mqtt_trigger'
| 'sqs_trigger'
| 'gcp_trigger'
let meta: Meta | undefined = undefined
export let fullNamePlaceholder: string | undefined = undefined
export let namePlaceholder = ''
@@ -262,6 +265,11 @@
workspace: $workspaceStore!,
path: path
})
} else if (kind === 'gcp_trigger') {
return await GcpTriggerService.existsGcpTrigger({
workspace: $workspaceStore!,
path: path
})
} else {
return false
}
@@ -31,6 +31,7 @@
| 'mqtt_trigger'
| 'sqs_trigger'
| 'postgres_trigger'
| 'gcp_trigger'
let kind: Kind
let path: string = ''
@@ -17,6 +17,7 @@
| 'kafka'
| 'mqtt'
| 'sqs'
| 'gcp'
| 'nats' = 'webhooks'
export let flow_json: any | undefined = undefined
export let simplfiedPoll: boolean = false
@@ -61,6 +62,7 @@
<slot slot="nats" name="nats" />
<slot slot="mqtt" name="mqtt" />
<slot slot="sqs" name="sqs" />
<slot slot="gcp" name="gcp" />
<slot slot="emails" name="emails" />
<slot slot="schedules" name="schedules" />
<slot slot="cli" name="cli" />
@@ -32,6 +32,7 @@
| 'nats'
| 'mqtt'
| 'sqs'
| 'gcp'
>('webhooks')
const simplifiedPoll = writable(false)
@@ -71,6 +72,7 @@
<slot slot="nats" name="nats" />
<slot slot="mqtt" name="mqtt" />
<slot slot="sqs" name="sqs" />
<slot slot="gcp" name="gcp" />
<slot slot="emails" name="emails" />
<slot slot="schedules" name="schedules" />
<slot slot="cli" name="cli" />
@@ -120,6 +122,7 @@
<slot slot="nats" name="nats" />
<slot slot="mqtt" name="mqtt" />
<slot slot="sqs" name="sqs" />
<slot slot="gcp" name="gcp" />
<slot slot="emails" name="emails" />
<slot slot="schedules" name="schedules" />
<slot slot="cli" name="cli" />
@@ -15,6 +15,7 @@
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon } from '../icons'
import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte'
export let triggerSelected:
| 'webhooks'
@@ -28,17 +29,19 @@
| 'nats'
| 'sqs'
| 'mqtt'
| 'gcp'
| 'scheduledPoll' = 'webhooks'
export let simplfiedPoll: boolean = false
export let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' = 'kafka'
export let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' | 'gcp' = 'kafka'
$: {
if (
triggerSelected === 'kafka' ||
triggerSelected === 'nats' ||
triggerSelected === 'sqs' ||
triggerSelected === 'mqtt'
triggerSelected === 'mqtt' ||
triggerSelected === 'gcp'
) {
eventStreamType = triggerSelected
}
@@ -80,7 +83,7 @@
Postgres
</span>
</Tab>
<Tab value="kafka" otherValues={['nats', 'sqs', 'mqtt']}>
<Tab value="kafka" otherValues={['nats', 'sqs', 'mqtt', 'gcp']}>
<span class="flex flex-row gap-2 items-center text-xs">
<PlugZap size={12} />
Event streams
@@ -113,13 +116,14 @@
<slot name="websockets" />
{:else if triggerSelected === 'postgres'}
<slot name="postgres" />
{:else if triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs' || triggerSelected === 'mqtt'}
{:else if triggerSelected === 'kafka' || triggerSelected === 'nats' || triggerSelected === 'sqs' || triggerSelected === 'mqtt' || triggerSelected === 'gcp'}
<div class="m-1.5">
<ToggleButtonGroup bind:selected={eventStreamType} let:item>
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} {item} />
<ToggleButton value="nats" label="NATS" icon={NatsIcon} {item} />
<ToggleButton value="mqtt" label="MQTT" icon={MqttIcon} {item} />
<ToggleButton value="sqs" label="SQS" icon={AwsIcon} {item} />
<ToggleButton value="gcp" label="GCP Pub/Sub" icon={GoogleCloudIcon} {item} />
</ToggleButtonGroup>
</div>
{#if eventStreamType === 'kafka'}
@@ -130,6 +134,8 @@
<slot name="sqs" />
{:else if eventStreamType === 'mqtt'}
<slot name="mqtt" />
{:else if eventStreamType === 'gcp'}
<slot name="gcp" />
{/if}
{:else if triggerSelected === 'cli'}
<slot name="cli" />
@@ -11,6 +11,7 @@
import { FlowService, ScriptService } from '$lib/gen'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { MqttIcon, NatsIcon, KafkaIcon, AwsIcon } from '$lib/components/icons'
import GoogleCloudIcon from '$lib/components/icons/GoogleCloudIcon.svelte'
const { selectedTrigger, triggersCount } = getContext<TriggerContext>('TriggerContext')
@@ -31,6 +32,7 @@
| 'eventStreams'
| 'postgres'
| 'sqs'
| 'gcp'
)[] = showOnlyWithCount
? ['webhooks', 'schedules', 'routes', 'websockets', 'kafka', 'nats', 'emails']
: ['webhooks', 'schedules', 'routes', 'websockets', 'eventStreams', 'emails']
@@ -69,6 +71,7 @@
nats: { icon: NatsIcon, countKey: 'nats_count' },
mqtt: { icon: MqttIcon, countKey: 'mqtt_count' },
sqs: { icon: AwsIcon, countKey: 'sqs_count' },
gcp: { icon: GoogleCloudIcon, countKey: 'gcp_count' },
eventStreams: { icon: PlugZap }
}
@@ -80,7 +83,7 @@
{#each triggersToDisplay as type}
{@const { icon, countKey } = triggerTypeConfig[type]}
{#if (!showOnlyWithCount || ((countKey && $triggersCount?.[countKey]) || 0) > 0) && !(type === 'sqs' && !$enterpriseLicense) && !(type === 'kafka' && !$enterpriseLicense) && !(type === 'nats' && !$enterpriseLicense) && !(type === 'mqtt')}
{#if (!showOnlyWithCount || ((countKey && $triggersCount?.[countKey]) || 0) > 0) && !(type === 'gcp' && !$enterpriseLicense) && !(type === 'sqs' && !$enterpriseLicense) && !(type === 'kafka' && !$enterpriseLicense) && !(type === 'nats' && !$enterpriseLicense) && !(type === 'mqtt')}
<Popover>
<svelte:fragment slot="text">{camelCaseToWords(type)}</svelte:fragment>
<TriggerButton
@@ -94,7 +97,8 @@
($selectedTrigger === 'kafka' ||
$selectedTrigger === 'nats' ||
$selectedTrigger === 'sqs' ||
$selectedTrigger === 'mqtt')))}
$selectedTrigger === 'mqtt' ||
$selectedTrigger === 'gcp')))}
>
{#if countKey}
<TriggerCount count={$triggersCount?.[countKey]} />
@@ -1,21 +1,33 @@
<script lang="ts">
export let height = '24px'
export let width = '24px'
export let height = '20px'
export let width = '20px'
</script>
<svg {width} {height} viewBox="0 -25 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M170.2517,56.8186 L192.5047,34.5656 L193.9877,25.1956 C153.4367,-11.6774 88.9757,-7.4964 52.4207,33.9196 C42.2667,45.4226 34.7337,59.7636 30.7167,74.5726 L38.6867,73.4496 L83.1917,66.1106 L86.6277,62.5966 C106.4247,40.8546 139.8977,37.9296 162.7557,56.4286 L170.2517,56.8186 Z" fill="#EA4335">
</path>
<path d="M224.2048,73.9182 C219.0898,55.0822 208.5888,38.1492 193.9878,25.1962 L162.7558,56.4282 C175.9438,67.2042 183.4568,83.4382 183.1348,100.4652 L183.1348,106.0092 C198.4858,106.0092 210.9318,118.4542 210.9318,133.8052 C210.9318,149.1572 198.4858,161.2902 183.1348,161.2902 L127.4638,161.2902 L121.9978,167.2242 L121.9978,200.5642 L127.4638,205.7952 L183.1348,205.7952 C223.0648,206.1062 255.6868,174.3012 255.9978,134.3712 C256.1858,110.1682 244.2528,87.4782 224.2048,73.9182" fill="#4285F4">
</path>
<path d="M71.8704,205.7957 L127.4634,205.7957 L127.4634,161.2897 L71.8704,161.2897 C67.9094,161.2887 64.0734,160.4377 60.4714,158.7917 L52.5844,161.2117 L30.1754,183.4647 L28.2234,191.0387 C40.7904,200.5277 56.1234,205.8637 71.8704,205.7957" fill="#34A853">
</path>
<path d="M71.8704,61.4255 C31.9394,61.6635 -0.2366,94.2275 0.0014,134.1575 C0.1344,156.4555 10.5484,177.4455 28.2234,191.0385 L60.4714,158.7915 C46.4804,152.4705 40.2634,136.0055 46.5844,122.0155 C52.9044,108.0255 69.3704,101.8085 83.3594,108.1285 C89.5244,110.9135 94.4614,115.8515 97.2464,122.0155 L129.4944,89.7685 C115.7734,71.8315 94.4534,61.3445 71.8704,61.4255" fill="#FBBC05">
</path>
</g>
</svg>
<svg
{width}
{height}
viewBox="0 -25 256 256"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
preserveAspectRatio="xMidYMid"
>
<g>
<path
d="M170.2517,56.8186 L192.5047,34.5656 L193.9877,25.1956 C153.4367,-11.6774 88.9757,-7.4964 52.4207,33.9196 C42.2667,45.4226 34.7337,59.7636 30.7167,74.5726 L38.6867,73.4496 L83.1917,66.1106 L86.6277,62.5966 C106.4247,40.8546 139.8977,37.9296 162.7557,56.4286 L170.2517,56.8186 Z"
fill="#B0B0B0"
/>
<path
d="M224.2048,73.9182 C219.0898,55.0822 208.5888,38.1492 193.9878,25.1962 L162.7558,56.4282 C175.9438,67.2042 183.4568,83.4382 183.1348,100.4652 L183.1348,106.0092 C198.4858,106.0092 210.9318,118.4542 210.9318,133.8052 C210.9318,149.1572 198.4858,161.2902 183.1348,161.2902 L127.4638,161.2902 L121.9978,167.2242 L121.9978,200.5642 L127.4638,205.7952 L183.1348,205.7952 C223.0648,206.1062 255.6868,174.3012 255.9978,134.3712 C256.1858,110.1682 244.2528,87.4782 224.2048,73.9182"
fill="#D0D0D0"
/>
<path
d="M71.8704,205.7957 L127.4634,205.7957 L127.4634,161.2897 L71.8704,161.2897 C67.9094,161.2887 64.0734,160.4377 60.4714,158.7917 L52.5844,161.2117 L30.1754,183.4647 L28.2234,191.0387 C40.7904,200.5277 56.1234,205.8637 71.8704,205.7957"
fill="#E0E0E0"
/>
<path
d="M71.8704,61.4255 C31.9394,61.6635 -0.2366,94.2275 0.0014,134.1575 C0.1344,156.4555 10.5484,177.4455 28.2234,191.0385 L60.4714,158.7915 C46.4804,152.4705 40.2634,136.0055 46.5844,122.0155 C52.9044,108.0255 69.3704,101.8085 83.3594,108.1285 C89.5244,110.9135 94.4614,115.8515 97.2464,122.0155 L129.4944,89.7685 C115.7734,71.8315 94.4534,61.3445 71.8704,61.4255"
fill="#FFFFFF"
/>
</g>
</svg>
@@ -89,6 +89,11 @@
id: 'triggers',
href: `${base}/sqs_triggers`
},
{
label: 'GCP Pub/Sub triggers',
id: 'triggers',
href: `${base}/gcp_triggers`
},
{
label: 'MQTT triggers',
id: 'triggers',
@@ -60,6 +60,7 @@
MeltButton
} from '$lib/components/meltComponents'
import MenuButton from './MenuButton.svelte'
import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte'
export let numUnacknowledgedCriticalAlerts = 0
@@ -136,13 +137,20 @@
disabled: $userStore?.operator || !$enterpriseLicense,
kind: 'sqs'
},
{
label: 'GCP Pub/Sub' + ($enterpriseLicense ? '' : ' (EE)'),
href: '/gcp_triggers',
icon: GoogleCloudIcon,
disabled: $userStore?.operator || !$enterpriseLicense,
kind: 'gcp'
},
{
label: 'MQTT',
href: '/mqtt_triggers',
icon: MqttIcon,
disabled: $userStore?.operator,
kind: 'mqtt'
},
}
]
$: extraTriggerLinks = defaultExtraTriggerLinks.filter((link) => {
+3
View File
@@ -54,6 +54,7 @@ export type TriggerKind =
| 'postgres'
| 'mqtt'
| 'sqs'
| 'gcp'
export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): TriggerKind {
switch (kind) {
case 'webhook':
@@ -74,6 +75,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg
return 'sqs'
case 'postgres':
return 'postgres'
case 'gcp':
return 'gcp'
default:
throw new Error(`Unknown CaptureTriggerKind: ${kind}`)
}
@@ -11,6 +11,7 @@
import AwsIcon from '../icons/AwsIcon.svelte'
import DropdownV2 from '$lib/components/DropdownV2.svelte'
import MqttIcon from '../icons/MqttIcon.svelte'
import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte'
export let small = false
@@ -45,6 +46,12 @@
action: () => handleClick('sqs'),
disabled: !$enterpriseLicense
},
{
icon: GoogleCloudIcon,
displayName: 'GCP Pub/Sub',
action: () => handleClick('gcp'),
disabled: !$enterpriseLicense
},
{
icon: MqttIcon,
displayName: 'MQTT',
@@ -6,6 +6,7 @@
isFlow: boolean
path: string
connectionInfo: ConnectionInfo | undefined
loading?: boolean
}
</script>
@@ -70,7 +71,11 @@
<div class="flex flex-col gap-1 mb-4">
<div class="flex flex-row items-center justify-start gap-1">
<PulseButton bind:this={pulseButton} numberOfPulses={1} pulseDuration={1}>
<AnimatedButton animate={captureInfo.active} baseRadius="6px" wrapperClasses="ml-[-2px]">
<AnimatedButton
animate={captureInfo.active || captureInfo.loading}
baseRadius="6px"
wrapperClasses="ml-[-2px]"
>
<Button
size="xs2"
on:click={() => dispatch('captureToggle', {})}
@@ -18,6 +18,7 @@
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
import type { Capture } from '$lib/gen'
import { AwsIcon, MqttIcon } from '../icons'
import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte'
export let path: string
export let hasPreprocessor = false
@@ -169,7 +170,8 @@
kafka: KafkaIcon,
mqtt: MqttIcon,
sqs: AwsIcon,
postgres: DatabaseIcon
postgres: DatabaseIcon,
gcp: GoogleCloudIcon
}
function handleKeydown(event: KeyboardEvent) {
@@ -19,6 +19,7 @@
import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte'
import { invalidRelations } from './postgres/utils'
import { DEFAULT_V3_CONFIG, DEFAULT_V5_CONFIG } from './mqtt/constant'
import GcpTriggerEditorConfigSection from './gcp/GcpTriggerEditorConfigSection.svelte'
export let isFlow: boolean
export let path: string
@@ -28,6 +29,7 @@
export let showCapture = false
export let data: any = {}
export let connectionInfo: ConnectionInfo | undefined = undefined
export let loading = false
export let args: Record<string, any> = {}
export let captureTable: CaptureTable | undefined = undefined
@@ -37,7 +39,6 @@
sendUserToast('Table to track must be set', true)
return false
}
if (
invalidRelations(args.publication.table_to_track, {
showError: true,
@@ -48,7 +49,8 @@
}
}
try {
await CaptureService.setCaptureConfig({
loading = true
args = await CaptureService.setCaptureConfig({
requestBody: {
trigger_kind: captureType,
path,
@@ -57,8 +59,10 @@
},
workspace: $workspaceStore!
})
loading = false
return true
} catch (error) {
loading = false
sendUserToast(error.body, true)
return false
}
@@ -69,19 +73,20 @@
let captureConfigs: {
[key: string]: CaptureConfig
} = {}
const STREAMING_CAPTURES = ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp']
async function getCaptureConfigs() {
const captureConfigsList = await CaptureService.getCaptureConfigs({
workspace: $workspaceStore!,
runnableKind: isFlow ? 'flow' : 'script',
path
})
captureConfigs = captureConfigsList.reduce((acc, c) => {
acc[c.trigger_kind] = c
return acc
}, {})
const streamingCapture = ['postgres', 'websocket', 'kafka', 'sqs', 'mqtt']
if (streamingCapture.includes(captureType) && captureActive) {
if (isStreamingCapture() && captureActive) {
const config = captureConfigs[captureType]
if (config && config.error) {
const serverEnabled = getServerEnabled(config)
@@ -130,6 +135,7 @@
subscribe_topics: []
}
break
default:
args = {}
}
@@ -153,7 +159,6 @@
captureActive = false
} else {
const configSet = await setConfig()
if (configSet) {
capture()
}
@@ -162,11 +167,18 @@
let config: CaptureConfig | undefined
$: config = captureConfigs[captureType]
const streamingCaptures = ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats']
let cloudDisabled = streamingCaptures.includes(captureType) && isCloudHosted()
let cloudDisabled = STREAMING_CAPTURES.includes(captureType) && isCloudHosted()
function isStreamingCapture() {
if (captureType === 'gcp' && args.delivery_type === 'push') {
return false
}
return ['mqtt', 'sqs', 'websocket', 'postgres', 'kafka', 'nats', 'gcp'].includes(captureType)
}
function updateConnectionInfo(config: CaptureConfig | undefined, captureActive: boolean) {
if (streamingCaptures.includes(captureType) && config && captureActive) {
if (isStreamingCapture() && config && captureActive) {
const serverEnabled = getServerEnabled(config)
const connected = serverEnabled && !config.error
const message = connected
@@ -189,7 +201,8 @@
canHavePreprocessor,
isFlow,
path,
connectionInfo
connectionInfo,
loading: loading
}
$: args && (captureActive = false)
@@ -348,6 +361,28 @@
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'gcp'}
<GcpTriggerEditorConfigSection
can_write={true}
headless={true}
bind:gcp_resource_path={args.gcp_resource_path}
bind:topic_id={args.topic_id}
bind:subscription_id={args.subscription_id}
bind:cloud_subscription_id={args.subscription_id}
bind:create_update_subscription_id={args.subscription_id}
bind:delivery_config={args.delivery_config}
bind:delivery_type={args.delivery_type}
bind:subscription_mode={args.subscription_mode}
bind:base_endpoint={args.base_endpoint}
{showCapture}
{captureInfo}
bind:captureTable
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={handleCapture}
on:testWithArgs
/>
{/if}
</div>
{/key}
@@ -6,13 +6,14 @@
NatsTriggerService,
SqsTriggerService,
PostgresTriggerService,
WebsocketTriggerService
WebsocketTriggerService,
GcpTriggerService
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import Button from '../common/button/Button.svelte'
export let kind: 'websocket' | 'nats' | 'kafka' | 'postgres' | 'sqs' | 'mqtt'
export let kind: 'websocket' | 'nats' | 'kafka' | 'postgres' | 'sqs' | 'mqtt' | 'gcp'
export let args: Record<string, any>
export let noButton = false
export let testLoading: boolean = false
@@ -23,7 +24,8 @@
kafka: 'Kafka broker(s)',
sqs: 'SQS',
postgres: 'Postgres',
mqtt: 'MQTT broker'
mqtt: 'MQTT broker',
gcp: 'Google Cloud Pub/Sub'
}
let promise: CancelablePromise<any> | null = null
@@ -65,6 +67,11 @@
workspace: $workspaceStore!,
requestBody: args as any
})
} else if (kind === 'gcp') {
promise = GcpTriggerService.testGcpConnection({
workspace: $workspaceStore!,
requestBody: args as any
})
}
await promise
sendUserToast(`Successfully connected to ${kindToName[kind]}`)
@@ -15,11 +15,12 @@
import PostgresTriggersPanel from './postgres/PostgresTriggersPanel.svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import { KafkaIcon, MqttIcon, NatsIcon, AwsIcon } from '../icons'
import { KafkaIcon, MqttIcon, NatsIcon, AwsIcon, GoogleCloudIcon } from '../icons'
import KafkaTriggersPanel from './kafka/KafkaTriggersPanel.svelte'
import NatsTriggersPanel from './nats/NatsTriggersPanel.svelte'
import MqttTriggersPanel from './mqtt/MqttTriggersPanel.svelte'
import SqsTriggerPanel from './sqs/SqsTriggerPanel.svelte'
import GcpTriggerPanel from './gcp/GcpTriggerPanel.svelte'
export let noEditor: boolean
export let newItem = false
@@ -32,14 +33,15 @@
export let canHavePreprocessor: boolean = false
export let hasPreprocessor: boolean = false
export let args: Record<string, any> = {}
let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' = 'kafka'
let eventStreamType: 'kafka' | 'nats' | 'sqs' | 'mqtt' | 'gcp' = 'kafka'
$: {
if (
$selectedTrigger === 'kafka' ||
$selectedTrigger === 'nats' ||
$selectedTrigger === 'sqs' ||
$selectedTrigger === 'mqtt'
$selectedTrigger === 'mqtt' ||
$selectedTrigger === 'gcp'
) {
eventStreamType = $selectedTrigger
}
@@ -64,7 +66,7 @@
<Tab value="postgres" selectedClass="text-primary text-sm font-semibold">Postgres</Tab>
<Tab
value="kafka"
otherValues={['nats', 'sqs', 'mqtt']}
otherValues={['nats', 'sqs', 'mqtt', 'gcp']}
selectedClass="text-primary text-sm font-semibold"
>
Event streams
@@ -160,13 +162,14 @@
isEditor={true}
/>
</div>
{:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs' || $selectedTrigger === 'mqtt'}
{:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats' || $selectedTrigger === 'sqs' || $selectedTrigger === 'mqtt' || $selectedTrigger === 'gcp'}
<div class="p-4 flex flex-col gap-2">
<ToggleButtonGroup bind:selected={eventStreamType} let:item>
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} {item} />
<ToggleButton value="nats" label="NATS" icon={NatsIcon} {item} />
<ToggleButton value="mqtt" label="MQTT" icon={MqttIcon} {item} />
<ToggleButton value="sqs" label="SQS" icon={AwsIcon} {item} />
<ToggleButton value="gcp" label="GCP" icon={GoogleCloudIcon} {item} />
</ToggleButtonGroup>
{#if eventStreamType === 'kafka'}
<KafkaTriggersPanel
@@ -218,6 +221,19 @@
{canHavePreprocessor}
{hasPreprocessor}
/>
{:else if eventStreamType === 'gcp'}
<GcpTriggerPanel
on:applyArgs
on:addPreprocessor
on:updateSchema
on:testWithArgs
{newItem}
path={initialPath || fakeInitialPath}
{isFlow}
isEditor={true}
{canHavePreprocessor}
{hasPreprocessor}
/>
{/if}
</div>
{:else if $selectedTrigger === 'schedules'}
@@ -26,15 +26,16 @@
export let showCapture: boolean = false
const captureTypeLabels: Record<CaptureTriggerKind, string> = {
http: '+ New custom HTTP route',
websocket: '+ New WebSocket trigger',
http: 'custom HTTP route',
websocket: 'WebSocket trigger',
webhook: 'Webhook',
kafka: '+ New Kafka trigger',
kafka: 'Kafka trigger',
email: 'Email trigger',
nats: '+ New NATS trigger',
mqtt: '+ New MQTT trigger',
sqs: '+ New SQS trigger',
postgres: '+ New Postgres trigger'
nats: 'NATS trigger',
mqtt: 'MQTT trigger',
sqs: 'SQS trigger',
postgres: 'Postgres trigger',
gcp: 'GCP Pub/Sub trigger'
}
const { captureOn } = getContext<TriggerContext>('TriggerContext')
@@ -55,7 +56,11 @@
}
</script>
<Section label={captureTypeLabels[triggerType]} collapsable={!alwaysOpened} bind:collapsed>
<Section
label={`+ New ${captureTypeLabels[triggerType]}`}
collapsable={!alwaysOpened}
bind:collapsed
>
<svelte:fragment slot="action">
{#if !collapsed || alwaysOpened}
<div class="flex flex-row grow w-min-0 gap-2 items-center justify-end">
@@ -11,6 +11,7 @@
import MqttEditorConfigSection from './mqtt/MqttEditorConfigSection.svelte'
import SqsTriggerEditorConfigSection from './sqs/SqsTriggerEditorConfigSection.svelte'
import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte'
import GcpTriggerEditorConfigSection from './gcp/GcpTriggerEditorConfigSection.svelte'
export let triggerType: CaptureTriggerKind = 'webhook'
export let cloudDisabled: boolean = false
@@ -96,5 +97,20 @@
can_write={true}
showCapture={false}
/>
{:else if triggerType === 'gcp'}
<GcpTriggerEditorConfigSection
bind:gcp_resource_path={args.gcp_resource_path}
bind:topic_id={args.topic_id}
bind:subscription_id={args.subscription_id}
bind:delivery_config={args.delivery_config}
bind:delivery_type={args.delivery_type}
bind:subscription_mode={args.subscription_mode}
bind:cloud_subscription_id={args.subscription_id}
bind:create_update_subscription_id={args.subscription_id}
bind:base_endpoint={args.base_endpoint}
headless={true}
can_write={true}
showCapture={false}
/>
{/if}
</div>
@@ -0,0 +1,27 @@
<script lang="ts">
import { tick } from 'svelte'
import GcpTriggerEditorInner from './GcpTriggerEditorInner.svelte'
let open = false
export async function openEdit(ePath: string, isFlow: boolean) {
open = true
await tick()
drawer?.openEdit(ePath, isFlow)
}
export async function openNew(
is_flow: boolean,
initial_script_path?: string,
defaultValues?: Record<string, any>
) {
open = true
await tick()
drawer?.openNew(is_flow, initial_script_path, defaultValues)
}
let drawer: GcpTriggerEditorInner
</script>
{#if open}
<GcpTriggerEditorInner on:update bind:this={drawer} />
{/if}
@@ -0,0 +1,342 @@
<script lang="ts">
import Section from '$lib/components/Section.svelte'
import CaptureSection, { type CaptureInfo } from '../CaptureSection.svelte'
import CaptureTable from '../CaptureTable.svelte'
import Required from '$lib/components/Required.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
import { copyToClipboard, emptyStringTrimmed } from '$lib/utils'
import TestTriggerConnection from '../TestTriggerConnection.svelte'
import Subsection from '$lib/components/Subsection.svelte'
import {
GcpTriggerService,
type DeliveryType,
type PushConfig,
type SubscriptionMode
} from '$lib/gen'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import Label from '$lib/components/Label.svelte'
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
import { base } from '$lib/base'
import Toggle from '$lib/components/Toggle.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '$lib/defaults'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import Select from '$lib/components/apps/svelte-select/lib/Select.svelte'
import { workspaceStore } from '$lib/stores'
import { Button } from '$lib/components/common'
import { ClipboardCopy, RefreshCw } from 'lucide-svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
export let can_write: boolean = false
export let headless: boolean = false
export let showCapture: boolean = false
export let captureTable: CaptureTable | undefined = undefined
export let captureInfo: CaptureInfo | undefined = undefined
export let isValid: boolean = false
export let gcp_resource_path = ''
export let subscription_id = ''
export let topic_id = ''
export let delivery_type: DeliveryType | undefined = 'pull'
export let delivery_config: PushConfig | undefined
export let subscription_mode: SubscriptionMode = 'create_update'
export let base_endpoint: string = getBaseUrl(captureInfo)
export let path = captureInfo ? captureInfo.path : ''
let topic_items: string[] = []
let subscription_items: string[] = []
let darkMode = false
const DEFAULT_PUSH_CONFIG: PushConfig = {
audience: '',
authenticate: false,
base_endpoint
}
async function loadAllPubSubTopicsFromProject() {
if (!emptyStringTrimmed(gcp_resource_path)) {
topic_items = await GcpTriggerService.listGoogleTopics({
workspace: $workspaceStore!,
path: gcp_resource_path
})
}
}
async function loadAllSubscriptionFromGooglePubSubTopic() {
if (!emptyStringTrimmed(gcp_resource_path) && !emptyStringTrimmed(topic_id)) {
subscription_items = await GcpTriggerService.listAllTgoogleTopicSubscriptions({
workspace: $workspaceStore!,
path: gcp_resource_path,
requestBody: {
topic_id
}
})
}
}
export let cloud_subscription_id = ''
export let create_update_subscription_id = ''
$: isValid = !emptyStringTrimmed(gcp_resource_path) && !emptyStringTrimmed(topic_id)
$: if (!delivery_type) {
delivery_type = 'pull'
}
$: if (!delivery_config) {
delivery_config = DEFAULT_PUSH_CONFIG
}
function getBaseUrl(captureInfo: CaptureInfo | undefined) {
if (captureInfo) {
return `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/gcp/${
captureInfo.isFlow ? 'flow' : 'script'
}`
} else {
return `${window.location.origin}${base}/api/gcp/w/${$workspaceStore!}`
}
}
$: !base_endpoint && (base_endpoint = getBaseUrl(captureInfo))
</script>
<DarkModeObserver bind:darkMode />
<div>
{#if showCapture && captureInfo}
{@const captureURL = `${base_endpoint}/${path}`}
<CaptureSection
captureType="gcp"
disabled={!isValid}
{captureInfo}
on:captureToggle
on:applyArgs
on:updateSchema
on:addPreprocessor
on:testWithArgs
bind:captureTable
>
{#if delivery_type === 'push'}
<Label label="URL">
<ClipboardPanel content={captureURL} disabled={!captureInfo.active} />
</Label>
{/if}
</CaptureSection>
{/if}
<Section label="GCP Pub/Sub" {headless}>
<div class="flex flex-col w-full gap-4">
<Subsection label="Connection setup">
<div class="flex flex-col gap-1 mt-2">
<ResourcePicker
on:change={() => {
loadAllPubSubTopicsFromProject()
}}
resourceType="gcloud"
bind:value={gcp_resource_path}
/>
{#if !emptyStringTrimmed(gcp_resource_path)}
<TestTriggerConnection kind="gcp" args={{ gcp_resource_path }} />
{/if}
</div>
</Subsection>
<div class="flex flex-col gap-1">
<Subsection
label="Topic"
tooltip="Select the Pub/Sub topic that this subscription will be attached to. Messages published to this topic will be delivered to your subscription."
>
<div class="flex gap-1 mt-2">
<Select
class="grow shrink max-w-full"
on:change={(e) => {
topic_id = e.detail.value
loadAllSubscriptionFromGooglePubSubTopic()
}}
on:clear={() => {
topic_id = ''
}}
value={topic_id}
items={topic_items}
placeholder="Choose a topic"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles}
portal={false}
/>
<Button
disabled={!can_write}
variant="border"
color="light"
wrapperClasses="self-stretch"
on:click={loadAllPubSubTopicsFromProject}
startIcon={{ icon: RefreshCw }}
iconOnly
/>
</div>
</Subsection>
</div>
{#if !emptyStringTrimmed(gcp_resource_path) && !emptyStringTrimmed(topic_id)}
<Section
label="Subscription"
tooltip="Choose whether to create or update a Pub/Sub subscription, or link an existing one from your Google Cloud project."
documentationLink="https://www.windmill.dev/docs/core_concepts/gcp_triggers#subscription-setup"
>
<div class="flex flex-col gap-3">
<ToggleButtonGroup
bind:selected={subscription_mode}
on:selected={(e) => {
if (e.detail === 'existing' && subscription_items.length === 0) {
loadAllSubscriptionFromGooglePubSubTopic()
}
}}
let:item
>
<ToggleButton
label="Create/Update"
value="create_update"
tooltip="Create a new subscription or update an existing one with custom settings"
showTooltipIcon
{item}
/>
<ToggleButton
label="Existing subscription"
value="existing"
tooltip="Select an existing subscription from GCP Pub/Sub"
showTooltipIcon
{item}
/>
</ToggleButtonGroup>
{#if subscription_mode === 'create_update'}
<Subsection
label="Subscription id"
tooltip="The unique identifier for the Pub/Sub subscription. Leave it empty to let the system automatically generate a subscription ID, or provide a custom one if needed."
>
<div class="mt-2">
<input
type="text"
autocomplete="off"
placeholder="Enter subscription ID (leave empty to auto-generate)"
bind:value={create_update_subscription_id}
on:input={(event) => {
subscription_id = event?.currentTarget.value
}}
/>
</div>
</Subsection>
<div class="flex flex-col gap-2">
<Subsection
label="Delivery type"
tooltip="Select the delivery type for the Pub/Sub subscription. If the subscription already exists and you want to keep it as-is, choose the same delivery type as in Google Cloud. You can switch the type here if the API allows it — otherwise, make the change directly in Google Cloud."
>
<div class="flex flex-col gap-2 mt-2">
<ToggleButtonGroup bind:selected={delivery_type} let:item>
<ToggleButton
label="Pull"
tooltip="Create a subscription where your service will pull messages from the queue. Suitable for services that periodically check for new messages."
value="pull"
showTooltipIcon
{item}
/>
<ToggleButton
label="Push"
tooltip="Windmill will auto-generate a push endpoint for this subscription. You must not modify this endpoint in Google Cloud, as it is managed internally by Windmill."
showTooltipIcon
value="push"
{item}
/>
</ToggleButtonGroup>
</div>
</Subsection>
{#if delivery_type === 'push' && delivery_config}
<div class="flex flex-col gap-3 mt-1">
<Subsection
label="URL"
tooltip="The URL of the service that receives push messages."
>
<div class="flex gap-2">
<input
type="text"
autocomplete="off"
placeholder="URL"
disabled
value={`${base_endpoint}/${path}`}
/>
<Button
on:click={() => copyToClipboard(`${base_endpoint}/${path}`)}
color="dark"
size="xs"
startIcon={{ icon: ClipboardCopy }}
>
Copy URL
</Button>
</div>
</Subsection>
<Subsection
label="Audience"
tooltip="Provide the expected audience value for verifying OIDC tokens in push requests. If
left empty, the URL of the endpoint will be used as the default audience"
>
<input
type="text"
autocomplete="off"
placeholder="audience"
bind:value={delivery_config.audience}
disabled={!can_write}
/>
</Subsection>
<Subsection label="Authenticate">
<p class="text-xs mb-2 text-tertiary">
Enable Google Cloud authentication for push delivery using a verified token.<Required
required={true}
/>
</p>
<Toggle bind:checked={delivery_config.authenticate} />
</Subsection>
</div>
{/if}
</div>
{:else if subscription_mode === 'existing'}
<div class="flex flex-col gap-3">
<div class="flex gap-1">
<Select
class="grow shrink max-w-full"
on:change={(e) => {
subscription_id = e.detail.value
cloud_subscription_id = e.detail.value
}}
on:clear={() => {
subscription_id = ''
}}
value={cloud_subscription_id}
items={subscription_items}
placeholder="Choose a subscription"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={darkMode
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
: SELECT_INPUT_DEFAULT_STYLE.containerStyles}
portal={false}
/>
<Button
disabled={!can_write}
variant="border"
color="light"
wrapperClasses="self-stretch"
on:click={loadAllSubscriptionFromGooglePubSubTopic}
startIcon={{ icon: RefreshCw }}
iconOnly
/>
</div>
<Alert title="Push Subscription URL Requirements" type="warning">
If the subscription uses <strong>push delivery</strong>, its endpoint URL must
match the following format: <strong>{base_endpoint}/*</strong>, meaning it must
start with
<strong>{base_endpoint}</strong> followed by any path segment.
</Alert>
</div>
{/if}
</div>
</Section>
{/if}
</div>
</Section>
</div>
@@ -0,0 +1,293 @@
<script lang="ts">
import { Alert, Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Path from '$lib/components/Path.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { Loader2, Save } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import {
GcpTriggerService,
type DeliveryType,
type PushConfig,
type SubscriptionMode
} from '$lib/gen'
import Section from '$lib/components/Section.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import Required from '$lib/components/Required.svelte'
import GcpTriggerEditorConfigSection from './GcpTriggerEditorConfigSection.svelte'
import { base } from '$app/paths'
let is_loading = false
let drawer: Drawer
let is_flow: boolean = false
let initialPath = ''
let edit = true
let delivery_type: DeliveryType = 'pull'
let itemKind: 'flow' | 'script' = 'script'
let script_path = ''
let initialScriptPath = ''
let fixedScriptPath = ''
let path: string = ''
let pathError = ''
let enabled = false
let dirtyPath = false
let can_write = true
let drawerLoading = true
let topic_id: string = ''
let gcp_resource_path: string = ''
let subscription_id: string = ''
let isValid = false
let delivery_config: PushConfig | undefined = undefined
let subscription_mode: SubscriptionMode = 'create_update'
const dispatch = createEventDispatcher()
$: is_flow = itemKind === 'flow'
export async function openEdit(ePath: string, isFlow: boolean) {
drawerLoading = true
try {
drawer?.openDrawer()
initialPath = ePath
itemKind = isFlow ? 'flow' : 'script'
edit = true
dirtyPath = false
await loadTrigger()
} catch (err) {
sendUserToast(`Could not load GCP Pub/Sub trigger: ${err.body}`, true)
} finally {
drawerLoading = false
}
}
export async function openNew(
nis_flow: boolean,
fixedScriptPath_?: string,
defaultValues?: Record<string, any>
) {
drawerLoading = true
try {
drawer?.openDrawer()
is_flow = nis_flow
itemKind = nis_flow ? 'flow' : 'script'
initialScriptPath = ''
fixedScriptPath = fixedScriptPath_ ?? ''
script_path = fixedScriptPath
gcp_resource_path = defaultValues?.gcp_resource_path ?? ''
delivery_type = defaultValues?.delivery_type ?? 'pull'
delivery_config = defaultValues?.delivery_config ?? undefined
subscription_id = ''
topic_id = defaultValues?.topic_id
subscription_mode = defaultValues?.subscription_mode ?? 'create_update'
path = ''
initialPath = ''
edit = false
dirtyPath = false
} finally {
drawerLoading = false
}
}
async function loadTrigger(): Promise<void> {
try {
const s = await GcpTriggerService.getGcpTrigger({
workspace: $workspaceStore!,
path: initialPath
})
script_path = s.script_path
initialScriptPath = s.script_path
gcp_resource_path = s.gcp_resource_path
delivery_type = s.delivery_type
subscription_id = s.subscription_id
delivery_config = s.delivery_config
is_flow = s.is_flow
path = s.path
enabled = s.enabled
topic_id = s.topic_id
can_write = canWrite(s.path, s.extra_perms, $userStore)
} catch (error) {
sendUserToast(`Could not load GCP Pub/Sub trigger: ${error.body}`, true)
}
}
async function updateTrigger(): Promise<void> {
try {
is_loading = true
if (delivery_type === 'push') {
if (!delivery_config) {
sendUserToast('Must set route path when delivery type is push', true)
return
}
delivery_config.base_endpoint = `${window.location.origin}${base}`
} else {
delivery_config = undefined
}
if (edit) {
await GcpTriggerService.updateGcpTrigger({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
gcp_resource_path,
subscription_mode: {
subscription_mode,
subscription_id,
delivery_type,
delivery_config,
base_endpoint: `${window.location.origin}${base}`
},
topic_id,
path,
script_path,
enabled,
is_flow
}
})
sendUserToast(`GCP Pub/Sub trigger ${path} updated`)
} else {
await GcpTriggerService.createGcpTrigger({
workspace: $workspaceStore!,
requestBody: {
gcp_resource_path,
subscription_mode: {
subscription_mode,
subscription_id,
delivery_type,
delivery_config: delivery_config,
base_endpoint: `${window.location.origin}${base}`
},
topic_id,
path,
script_path,
enabled: true,
is_flow
}
})
sendUserToast(`GCP Pub/Sub trigger ${path} created`)
}
if (!$usedTriggerKinds.includes('gcp')) {
$usedTriggerKinds = [...$usedTriggerKinds, 'gcp']
}
dispatch('update')
drawer.closeDrawer()
is_loading = false
} catch (error) {
is_loading = false
sendUserToast(error.body, true)
}
}
</script>
<Drawer size="800px" bind:this={drawer}>
<DrawerContent
title={edit
? can_write
? `Edit GCP Pub/Sub trigger ${initialPath}`
: `GCP Pub/Sub trigger ${initialPath}`
: 'New GCP Pub/Sub trigger'}
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
{#if !drawerLoading && can_write}
{#if edit}
<div class="mr-8 center-center -mt-1">
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'enable', left: 'disable' }}
on:change={async (e) => {
sendUserToast(
`${e.detail ? 'enabled' : 'disabled'} GCP Pub/Sub trigger ${initialPath}`
)
}}
/>
</div>
{/if}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' || emptyString(script_path) || !isValid || !can_write}
on:click={updateTrigger}
loading={is_loading}
>
Save
</Button>
{/if}
</svelte:fragment>
{#if drawerLoading}
<div class="flex flex-col items-center justify-center h-full w-full">
<Loader2 size="50" class="animate-spin" />
<p>Loading...</p>
</div>
{:else}
<div class="flex flex-col gap-5">
<Alert title="Info" type="info">
{#if edit}
Changes can take up to 30 seconds to take effect.
{:else}
New GCP Pub/Sub trigger can take up to 30 seconds to start listening.
{/if}
</Alert>
</div>
<div class="flex flex-col gap-12 mt-6">
<div class="flex flex-col gap-4">
<Label label="Path">
<Path
bind:dirty={dirtyPath}
bind:error={pathError}
bind:path
{initialPath}
checkInitialPathExistence={!edit}
namePlaceholder="gcp_trigger"
kind="gcp_trigger"
disabled={!can_write}
/>
</Label>
</div>
<Section label="Runnable">
<p class="text-xs mb-1 text-tertiary">
Pick a script or flow to be triggered <Required required={true} />
</p>
<div class="flex flex-row mb-2">
<ScriptPicker
disabled={fixedScriptPath != '' || !can_write}
initialPath={fixedScriptPath || initialScriptPath}
kinds={['script']}
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
/>
{#if emptyString(script_path)}
<Button
btnClasses="ml-4 mt-2"
color="dark"
size="xs"
href={itemKind === 'flow' ? '/flows/add?hub=68' : '/scripts/add?hub=hub%2F14251'}
target="_blank">Create from template</Button
>
{/if}
</div>
</Section>
<GcpTriggerEditorConfigSection
bind:isValid
bind:gcp_resource_path
bind:subscription_id
bind:delivery_type
bind:delivery_config
bind:topic_id
bind:subscription_mode
bind:path={path}
cloud_subscription_id={subscription_id}
create_update_subscription_id={subscription_id}
{can_write}
headless={true}
/>
</div>
{/if}
</DrawerContent>
</Drawer>
@@ -0,0 +1,131 @@
<script lang="ts">
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { GcpTriggerService, type GcpTrigger } from '$lib/gen'
import { canWrite } from '$lib/utils'
import { getContext, onMount } from 'svelte'
import { isCloudHosted } from '$lib/cloud'
import Section from '$lib/components/Section.svelte'
import { Alert } from '$lib/components/common'
import Description from '$lib/components/Description.svelte'
import type { TriggerContext } from '$lib/components/triggers'
import TriggersEditorSection from '../TriggersEditorSection.svelte'
import GcpTriggerEditor from './GcpTriggerEditor.svelte'
export let isFlow: boolean
export let path: string
export let newItem: boolean = false
export let isEditor: boolean = false
export let canHavePreprocessor: boolean = false
export let hasPreprocessor: boolean = false
let gcpTriggerEditor: GcpTriggerEditor
let openForm = true
let dontCloseOnLoad = false
$: path && loadTriggers()
const { triggersCount, selectedTrigger, defaultValues } =
getContext<TriggerContext>('TriggerContext')
onMount(() => {
if (
defaultValues &&
$selectedTrigger === 'gcp' &&
Object.keys($defaultValues ?? {}).length > 0
) {
gcpTriggerEditor.openNew(isFlow, path, $defaultValues)
defaultValues.set(undefined)
}
})
let gcpTriggers: (GcpTrigger & { canWrite: boolean })[] | undefined = undefined
export async function loadTriggers() {
try {
gcpTriggers = (
await GcpTriggerService.listGcpTriggers({
workspace: $workspaceStore ?? '',
path,
isFlow
})
).map((x) => {
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
})
$triggersCount = { ...($triggersCount ?? {}), gcp_count: gcpTriggers?.length }
openForm = gcpTriggers?.length === 0 || dontCloseOnLoad
} catch (e) {
console.error('impossible to load GCP Pub/Sub triggers', e)
}
}
</script>
<GcpTriggerEditor
on:update={() => {
loadTriggers()
}}
bind:this={gcpTriggerEditor}
/>
{#if !$enterpriseLicense}
<Alert title="EE Only" type="warning" size="xs">
GCP Pub/Sub triggers are an enterprise only feature.
</Alert>
{:else if isCloudHosted()}
<Alert title="Not compatible with multi-tenant cloud" type="warning" size="xs">
GCP Pub/Sub triggers are disabled in the multi-tenant cloud.
</Alert>
{:else}
<div class="flex flex-col gap-4">
<Description link="https://cloud.google.com/pubsub/docs?hl=en">
GCP Pub/Sub triggers allow your scripts or flows to process messages from Google Cloud
Pub/Sub in real time. Each trigger listens to a Pub/Sub subscription and executes a script or
a flow when new messages are published to the corresponding topic.
</Description>
{#if !newItem && gcpTriggers && gcpTriggers.length > 0}
<Section label="GCP Pub/Sub">
<div class="flex flex-col gap-4">
<div class="flex flex-col divide-y pt-2">
{#each gcpTriggers as gcpTrigger (gcpTrigger.path)}
<div class="grid grid-cols-5 text-2xs items-center py-2">
<div class="col-span-2 truncate">{gcpTrigger.path}</div>
<div class="flex justify-end">
<button
on:click={() => gcpTriggerEditor?.openEdit(gcpTrigger.path, isFlow)}
class="px-2"
>
{#if gcpTrigger.canWrite}
Edit
{:else}
View
{/if}
</button>
</div>
</div>
{/each}
</div>
</div>
</Section>
{/if}
<TriggersEditorSection
on:applyArgs
on:saveTrigger={(e) => {
gcpTriggerEditor?.openNew(isFlow, path, e.detail.config)
}}
on:addPreprocessor
on:updateSchema
on:testWithArgs
cloudDisabled={false}
triggerType="gcp"
{isFlow}
{path}
{isEditor}
{canHavePreprocessor}
{hasPreprocessor}
{newItem}
{openForm}
bind:showCapture={dontCloseOnLoad}
/>
</div>
{/if}
@@ -4,12 +4,14 @@
import Tooltip from '$lib/components/Tooltip.svelte'
export let raw_string: boolean
export let wrap_body: boolean
export let wrap_body: boolean
</script>
<Label label="Raw body" class="w-full">
<svelte:fragment slot="header">
<Tooltip>
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/http_routing#body-processing-options"
>
Provides the raw JSON payload as a string under the 'raw_string' key. Required for custom
script authentication method and useful for signature verification or other advanced use
cases.
@@ -27,6 +29,7 @@
<Label label="Wrap body" class="w-full">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/http_routing#body-processing-options"
>Wraps the payload in an object under the 'body' key, useful for handling unknown payloads.</Tooltip
>
</svelte:fragment>
@@ -80,7 +80,7 @@
$: isValid = routeError === ''
$: fullRoute = getHttpRoute(route_path, workspaced_route, $workspaceStore ?? '')
$: fullRoute = getHttpRoute('r', route_path, workspaced_route, $workspaceStore ?? '')
$: !http_method && (http_method = 'post')
$: route_path === undefined && (route_path = '')
@@ -203,7 +203,9 @@
options={{
right: 'Prefix with workspace',
rightTooltip:
'Prefixes the route with the workspace ID (e.g., {base_url}/api/r/{workspace_id}/{route}). Note: deploying the HTTP trigger to another workspace updates the route workspace prefix accordingly.'
'Prefixes the route with the workspace ID (e.g., {base_url}/api/r/{workspace_id}/{route}). Note: deploying the HTTP trigger to another workspace updates the route workspace prefix accordingly.',
rightDocumentationLink:
'https://www.windmill.dev/docs/core_concepts/http_routing#workspace-prefix'
}}
/>
</div>
@@ -7,11 +7,12 @@ export const SIGNATURE_TEMPLATE_SCRIPT_HUB_PATH: string = `hub/${HUB_SCRIPT_ID}`
export const SIGNATURE_TEMPLATE_FLOW_HUB_ID = '67'
export function getHttpRoute(
route_prefix: string,
route_path: string | undefined,
workspaced_route: boolean,
workspace_id: string
workspace_id: string,
) {
return `${location.origin}${base}/api/r/${
return `${location.origin}${base}/api/${route_prefix}/${
isCloudHosted() || workspaced_route ? workspace_id + '/' : ''
}${route_path ?? ''}`
}
+34 -6
View File
@@ -635,7 +635,9 @@ export const TS_PREPROCESSOR_SCRIPT_INTRO = `/**
* The structure of \`wm_trigger\` and the main trigger arguments are specific to each trigger type:
* - Webhook/HTTP: \`(wm_trigger: { kind: 'http' | 'webhook', http?: { ... } }, body_key_1: any, body_key_2: any, ...)\`
* - Postgres: \`(wm_trigger: { kind: 'postgres' }, transaction_type: string, schema_name: string, table_name: string, row: any)\`
* - WebSocket/Kafka/NATS/SQS/MQTT: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs' | 'mqtt', [kind]: { ... } }, msg: string)\`
* - WebSocket/Kafka/NATS/SQS: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs', [kind]: { ... } }, msg: string)\`
* - MQTT: \`(wm_trigger: { kind: 'mqtt', [kind]: { ... } }, payload: Array<number>)\`
* - GCP: \`(wm_trigger: { kind: 'gcp', [kind]: { ... } }, payload: string)\`
* - Email: \`(wm_trigger: { kind: 'email' }, raw_email: string, parsed_email: { ... })\`
*
* The returned object defines the parameter values passed to \`main()\`.
@@ -655,7 +657,9 @@ export const TS_PREPROCESSOR_FLOW_INTRO = `/**
* The structure of \`wm_trigger\` and the main trigger arguments are specific to each trigger type:
* - Webhook/HTTP: \`(wm_trigger: { kind: 'http' | 'webhook', http?: { ... } }, body_key_1: any, body_key_2: any, ...)\`
* - Postgres: \`(wm_trigger: { kind: 'postgres' }, transaction_type: string, schema_name: string, table_name: string, row: any)\`
* - WebSocket/Kafka/NATS/SQS/MQTT: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs' | 'mqtt', [kind]: { ... } }, msg: string)\`
* - WebSocket/Kafka/NATS/SQS: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs', [kind]: { ... } }, msg: string)\`
* - MQTT: \`(wm_trigger: { kind: 'mqtt', [kind]: { ... } }, payload: Array<number>)\`
* - GCP: \`(wm_trigger: { kind: 'gcp', [kind]: { ... } }, payload: string)\`*
* - Email: \`(wm_trigger: { kind: 'email' }, raw_email: string, parsed_email: { ... })\`
*
* The returned object determines the parameter values passed to the flow.
@@ -673,7 +677,7 @@ export const TS_PREPROCESSOR_MODULE_CODE = `export async function preprocessor(
// The trigger metadata
wm_trigger: {
kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs' | 'mqtt',
kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs' | 'mqtt' | 'gcp',
http?: {
route: string // The route path, e.g. "/users/:id"
path: string // The actual path called, e.g. "/users/123"
@@ -722,6 +726,15 @@ export const TS_PREPROCESSOR_MODULE_CODE = `export async function preprocessor(
subscription_identifiers?: Array<number>,
content_type?: string
}
},
gcp?: {
message_id: string,
subscription: string,
ordering_key?: string,
attributes?: Record<string, string>,
delivery_type: "push" | "pull",
headers?: Record<string, string>,
publish_time?: string,
}
}
) {
@@ -769,7 +782,9 @@ export const PYTHON_PREPROCESSOR_SCRIPT_INTRO = `# Trigger preprocessor
# The structure of \`wm_trigger\` and the main trigger arguments are specific to each trigger type:
# - Webhook/HTTP: \`(wm_trigger: { kind: 'http' | 'webhook', http?: { ... } }, body_key_1: any, body_key_2: any, ...)\`
# - Postgres: \`(wm_trigger: { kind: 'postgres' }, transaction_type: string, schema_name: string, table_name: string, row: any)\`
# - WebSocket/Kafka/NATS/SQS/MQTT: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs' | 'mqtt', [kind]: { ... } }, msg: string)\`
# - WebSocket/Kafka/NATS/SQS: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs', [kind]: { ... } }, msg: string)\`
# - MQTT: \`(wm_trigger: { kind: 'mqtt', [kind]: { ... } }, payload: Array<number>)\`
# - GCP: \`(wm_trigger: { kind: 'gcp', [kind]: { ... } }, payload: string)\`
# - Email: \`(wm_trigger: { kind: 'email' }, raw_email: string, parsed_email: { ... })\`
#
# The returned object defines the parameter values passed to \`main()\`.
@@ -787,7 +802,9 @@ export const PYTHON_PREPROCESSOR_FLOW_INTRO = `# Trigger preprocessor
# plus trigger metadata in the \`wm_trigger\` object:
# - Webhook/HTTP: \`(wm_trigger: { kind: 'http' | 'webhook', http?: { ... } }, body_key_1: any, body_key_2: any, ...)\`
# - Postgres: \`(wm_trigger: { kind: 'postgres' }, transaction_type: string, schema_name: string, table_name: string, row: any)\`
# - WebSocket/Kafka/NATS/SQS/MQTT: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs' | 'mqtt', [kind]: { ... } }, msg: string)\`
# - WebSocket/Kafka/NATS/SQS: \`(wm_trigger: { kind: 'websocket' | 'kafka' | 'nats' | 'sqs', [kind]: { ... } }, msg: string)\`
# - MQTT: \`(wm_trigger: { kind: 'mqtt', [kind]: { ... } }, payload: Array<number>)\`
# - GCP: \`(wm_trigger: { kind: 'gcp', [kind]: { ... } }, payload: string)\`
# - Email: \`(wm_trigger: { kind: 'email' }, raw_email: string, parsed_email: { ... })\`
#
# The returned object determines the parameter values passed to the flow.
@@ -848,14 +865,25 @@ class Mqtt(TypedDict):
qos: int
v5: MqttV5Properties | None
class Gcp(TypedDict):
message_id: str
subscription: str
ordering_key: str | None
attributes: dict[str, str] | None
delivery_type: Literal["push", "pull"]
headers: dict[str, str] | None
publish_time: str | None
class WmTrigger(TypedDict):
kind: Literal["http", "email", "webhook", "websocket", "kafka", "nats", "postgres", "sqs", "mqtt"]
kind: Literal["http", "email", "webhook", "websocket", "kafka", "nats", "postgres", "sqs", "mqtt", "gcp"]
http: Http | None
websocket: Websocket | None
kafka: Kafka | None
nats: Nats | None
sqs: Sqs | None
mqtt: Mqtt | None
gcp: Gcp | None
def preprocessor(
# Replace this comment with the parameters received from the trigger.
+70 -5
View File
@@ -1,5 +1,6 @@
import { minimatch } from 'minimatch'
import {
GcpTriggerService,
HttpTriggerService,
KafkaTriggerService,
MqttTriggerService,
@@ -8,9 +9,11 @@ import {
ScheduleService,
SqsTriggerService,
WebsocketTriggerService,
type NewGcpTrigger,
type WorkspaceDeployUISettings
} from './gen'
import type { TriggerKind } from './components/triggers'
import { base } from './base'
type DeployUIType = 'script' | 'flow' | 'app' | 'resource' | 'variable' | 'secret' | 'trigger'
@@ -75,7 +78,11 @@ export async function existsTrigger(
return await PostgresTriggerService.existsPostgresTrigger(data)
} else if (triggerKind === 'sqs') {
return await SqsTriggerService.existsSqsTrigger(data)
} else if (triggerKind === 'websockets') {
}
else if (triggerKind === 'gcp') {
return await GcpTriggerService.existsGcpTrigger(data)
}
else if (triggerKind === 'websockets') {
return await WebsocketTriggerService.existsWebsocketTrigger(data)
} else if (triggerKind === 'nats') {
return await NatsTriggerService.existsNatsTrigger(data)
@@ -84,7 +91,7 @@ export async function existsTrigger(
}
throw new Error(
`Unexpected trigger kind ${triggerKind}. Allowed kinds are: routes, kafka, mqtt, postgres, sqs, websockets, nats, schedules.`
`Unexpected trigger kind ${triggerKind}. Allowed kinds are: routes, kafka, mqtt, postgres, sqs, gcp, websockets, nats, schedules.`
)
}
@@ -133,7 +140,36 @@ export async function getTriggersDeployData(kind: TriggerKind, path: string, wor
createFn: NatsTriggerService.createNatsTrigger,
updateFn: NatsTriggerService.updateNatsTrigger
}
} else if (kind === 'postgres') {
}
else if (kind === 'gcp') {
const gcpTrigger = await GcpTriggerService.getGcpTrigger({
workspace: workspace!,
path: path
})
gcpTrigger.subscription_id = ''
if (gcpTrigger.delivery_config) {
gcpTrigger.delivery_config.audience = ''
}
const data: NewGcpTrigger = {
subscription_mode: {
subscription_mode: 'create_update',
subscription_id: gcpTrigger.subscription_id,
delivery_type: gcpTrigger.delivery_type,
delivery_config: gcpTrigger.delivery_config,
base_endpoint: `${window.location.origin}${base}`
},
...gcpTrigger
}
return {
data,
createFn: GcpTriggerService.createGcpTrigger,
updateFn: GcpTriggerService.updateGcpTrigger
}
}
else if (kind === 'postgres') {
const postgresTrigger = await PostgresTriggerService.getPostgresTrigger({
workspace: workspace!,
path: path
@@ -285,7 +321,26 @@ export async function getTriggerValue(kind: TriggerKind, path: string, workspace
replication_slot_name,
publication_name
}
} else if (kind === 'websockets') {
}
else if (kind === 'gcp') {
const {
enabled,
script_path,
is_flow,
gcp_resource_path,
} = await GcpTriggerService.getGcpTrigger({
workspace: workspace!,
path: path
})
return {
enabled,
script_path,
is_flow,
gcp_resource_path,
}
}
else if (kind === 'websockets') {
const {
enabled,
script_path,
@@ -456,7 +511,17 @@ export async function getTriggerDependency(kind: TriggerKind, path: string, work
})
result = retrieveKindsValues({ resource_path: postgres_resource_path, script_path, is_flow })
} else if (kind === 'websockets') {
}
else if (kind === 'gcp') {
const { gcp_resource_path, script_path, is_flow } =
await GcpTriggerService.getGcpTrigger({
workspace: workspace!,
path: path
})
result = retrieveKindsValues({ resource_path: gcp_resource_path, script_path, is_flow })
}
else if (kind === 'websockets') {
const { script_path, is_flow, url, initial_messages } =
await WebsocketTriggerService.getWebsocketTrigger({
workspace: workspace!,

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