mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 00:01:55 +00:00
feat(triggers): add AMQP (RabbitMQ) trigger via lapin (#10230)
* feat(triggers): add AMQP (RabbitMQ) trigger using the lapin library Fixes WIN-2214 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(triggers): defer AMQP cross-workspace deploy pending utils-internal publish Revert the amqp_trigger additions to the shared windmill-utils-internal TriggerDeployKind and the frontend cross-workspace deploy adapter: the frontend installs the published npm package, which lacks the new kind until a release is cut. AMQP create/edit/delete/list/sync/capture are unaffected (they use local types); only cross-workspace deploy/merge of AMQP triggers waits on the package bump. Also document the at-most-once ack in the consumer loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): address AMQP review — at-least-once ack, workspace cascade, contracts - ack AMQP deliveries only after successful dispatch; nack+requeue on failure - add ON DELETE CASCADE workspace FK so amqp_trigger rows are cleaned on workspace deletion (and the listener stops) - fix the /amqp_triggers/test OpenAPI body and add amqp_trigger to WorkspaceDiffRow.kind - register AMQP in the generated workspace trigger tool (create_trigger) - drop banned $bindable defaults on optional props in the config section - add build_uri unit tests (encoding, ports, vhost) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): stop AMQP poison-message loop and reconnect on transient drops Chaos testing against a live RabbitMQ broker showed the previous nack(requeue) + immediate re-poll spun a tight redelivery loop (~1000 critical-error reports/sec) on a poison message, and any connection blip permanently disabled the trigger (lapin has no built-in reconnect). - on dispatch failure: nack+requeue then stop consuming; the listener framework re-lists the trigger after its ping goes stale (~15s), backing redelivery off to that cadence instead of a tight loop (verified: rate dropped from ~1000/s to ~1 per ~26s, message preserved) - on connection/stream error: stop and let the framework reconnect instead of disabling; persistent failures are still disabled via get_consumer (verified: a forced connection close now auto-reconnects and resumes) - finish the AI create-trigger action wiring for AMQP: add amqp to CreatedResourceTriggerKind, the action-card registry, and the drawer registry so the result card renders and its "Open" action works Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): complete AMQP frontend registries and defer merge rows - add amqp to capturableTriggerTypes (so AmqpCapture mounts), the Runs jobTriggerKinds filter, and CLOUD_DISABLED_TRIGGER_TYPES - wire AMQP into global AI chat mode: TRIGGER_KINDS, the request union, writeTriggerSchema, triggerServices, and the draft adapter - stop emitting actionable AMQP fork-comparison rows (revert amqp_trigger from TRIGGER_OR_SCHEDULE_TABLES) since cross-workspace deploy is deferred until windmill-utils-internal is published — avoids a deploy that fails with "Unknown kind: amqp_trigger" - use design-system TextInput instead of raw <input> in the config section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): complete AMQP session/draft registries and constrain prefetch - add amqp to the session-deploy, draft-compare, preview-router, and copilot workspace-item registries so AMQP drafts/deploys/nav/path resolution work - include amqp_count in the MoveDrawer attached-trigger rename warning - replace the raw prefetch <input> with a design-system TextInput bounded to an integer 1-65535 (backend u16) and block save on invalid values Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): make AMQP disconnect/reconnect consistent with the Kafka trigger lapin, like rdkafka, has no transparent reconnect, so the AMQP listener now mirrors the Kafka trigger's explicit reconnect loop instead of relying on the framework re-list (which disabled the trigger once get_consumer failed on a sustained outage): - get_consumer returns cheaply; consume owns a (re)connect loop that retries with a 30s backoff, reports a critical error every 10 failed attempts, and reports a recovered critical error once it reconnects — never disabling the trigger on a connectivity failure - a consumer/stream error breaks out to reconnect rather than disabling - dispatch failure still nacks+requeues (at-least-once) with a short backoff to avoid a tight poison-message loop, keeping the connection alive Verified against a live RabbitMQ broker: killing the broker keeps the trigger enabled and retrying (attempt N), and restarting it auto-reconnects (logs "reconnected after N attempts") and resumes dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): complete AMQP capture registries and constrain prefetch contract - add the 'amqp' case to triggerKindToTriggerType so opening the AMQP editor from a capture button no longer throws "Unknown TriggerKind: amqp" - register AmqpIcon in CaptureTable's icon map and add an AMQP entry to the script/flow CaptureButton menu - bound the OpenAPI prefetch_count to an integer 1-65535 (matches the Rust u16) and regenerate clients/prompts - require a non-empty exchange name when the exchange binding is enabled - build_uri: fall back to "/" on a blank vhost and bracket IPv6 hosts (+ tests) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(triggers): wire AMQP into pipeline graph, git-sync, and preprocessor types - asset_graph: discover attached amqp_trigger rows and emit an AMQP TriggerEdge so AMQP triggers render (and can be opened/deleted) on the data-pipeline canvas - frontend pipeline graph: add amqp to NativeTriggerKind, the add-trigger menu, node presentation, event-trigger set, annotation keywords, and the editor/service registrations - git-sync: add the amqp_trigger include pattern (+ test) so an AMQP git-sync deployment stages only its .amqp_trigger.* file, not an unrelated same-path object - preprocessor starters: add the AMQP event to the generated TS/Python/PHP trigger event types (kind/payload/exchange/routing_key/queue_name/redelivered/ delivery_tag) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): finish AMQP pipeline/parser wiring, prefetch validation, source lists - fix a stray edit that corrupted the pre-existing MqttTriggerEditor import ($lib/... path) in PipelineTriggerEditors.svelte - reject prefetch_count = 0 server-side in validate_config (RabbitMQ treats 0 as unlimited) and defensively skip basic_qos(0) in build_consumer (covers the capture path that bypasses CRUD validation) - recognize `// on amqp` in the canonical parser (TriggerSpec::Amqp) and add amqp to the CLI non-autorun/event-trigger sets so a pipeline cascade never runs an AMQP-only node as a manual root without an event - add amqp to the preprocessor intro lists and both pipeline AI instructions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(triggers): reject zero AMQP prefetch in all paths and finish guidance lists - extract a shared validate_amqp_options used by both CRUD validate_config and build_consumer, so capture configs (which bypass CRUD validation) also reject prefetch 0 instead of silently connecting with an unlimited buffer (+ unit tests for 0/1/65535/None) - add AMQP to the main script-writing preprocessor-sources prompt and the CLI triggers-skill guidance list Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(triggers): de-duplicate AMQP prefetch comment and fix GET response text - keep the zero-prefetch rationale only on the shared validate_amqp_options doc; drop the redundant call-site comments - correct the getAmqpTrigger OpenAPI 200 description ("deleted" -> "retrieved") Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to aaa6cb89b05b76139252c64f057e53b94d12ac60 This commit updates the EE repository reference after PR #680 was merged in windmill-ee-private. Previous ee-repo-ref: 5da5fd65aca9594b2611837a52e4677b544b0380 New ee-repo-ref: aaa6cb89b05b76139252c64f057e53b94d12ac60 Automated by sync-ee-ref workflow. * chore(migrations): consolidate the four AMQP migrations into one The table and the three enum ADD VALUE statements (trigger_kind, job_trigger_kind, draft_kind) are one atomic feature. ALTER TYPE ... ADD VALUE runs inside the migration transaction on PG >= 14 (Windmill's minimum) since the amqp_trigger table doesn't reference those enum types, so they can share a single migration instead of four. Verified applying cleanly in a single transaction on a fresh DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO amqp_trigger (\n amqp_resource_path,\n queue_name,\n exchange,\n options,\n workspace_id,\n path,\n script_path,\n is_flow,\n permissioned_as,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "trigger_mode",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"enabled",
|
||||
"disabled",
|
||||
"suspended"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "02e39bb9957d3ccfe3d573994cd281a22467010d2c4a0611c8ae02742336f433"
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT kind, path, script_path, is_flow FROM (\n SELECT 'schedule' AS kind, path, script_path, is_flow FROM schedule\n WHERE workspace_id = $1\n AND script_path IS NOT NULL\n UNION ALL\n SELECT 'email', path, script_path, is_flow FROM email_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'kafka', path, script_path, is_flow FROM kafka_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'mqtt', path, script_path, is_flow FROM mqtt_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'amqp', path, script_path, is_flow FROM amqp_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'nats', path, script_path, is_flow FROM nats_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'postgres', path, script_path, is_flow FROM postgres_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'sqs', path, script_path, is_flow FROM sqs_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'gcp', path, script_path, is_flow FROM gcp_trigger\n WHERE workspace_id = $1\n ) t\n WHERE ($2::text IS NULL OR script_path LIKE $2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "kind",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "05843e117361d6b7cd9da652596d3be7a970d602d3809396d9253d7e5dc88152"
|
||||
}
|
||||
+2
-1
@@ -36,7 +36,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH t1 AS (UPDATE websocket_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), t2 AS (UPDATE kafka_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), t3 AS (UPDATE postgres_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), t4 AS (UPDATE mqtt_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), t5 AS (UPDATE nats_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), t6 AS (UPDATE sqs_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), t7 AS (UPDATE amqp_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4) UPDATE gcp_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1cd16f93bdc90b24ceb1e86495069c3592218d34e58d617253e8b563bc661a7a"
|
||||
}
|
||||
+4
-2
@@ -40,7 +40,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -79,7 +80,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -129,7 +129,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -47,7 +47,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -35,7 +35,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -76,7 +77,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n amqp_trigger\n SET\n amqp_resource_path = $1,\n queue_name = $2,\n exchange = $3,\n options = $4,\n is_flow = $5,\n edited_by = $6,\n permissioned_as = $7,\n script_path = $8,\n path = $9,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE\n workspace_id = $10 AND\n path = $11\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5f2e66e53166840c68efbc521fe9393023e79f948976e6ffc0e9bfc94623393c"
|
||||
}
|
||||
+2
-1
@@ -38,7 +38,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -162,7 +162,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -47,7 +47,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE amqp_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "90da952a912de746abee641cc4268644853987e62cbcedc8d3a46626a60c9fb0"
|
||||
}
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"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 amqp_trigger WHERE workspace_id = $1) AS \"amqp_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 EXISTS(SELECT 1 FROM azure_trigger WHERE workspace_id = $1) AS \"azure_used!\",\n EXISTS(SELECT 1 FROM email_trigger WHERE workspace_id = $1) AS \"email_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'nextcloud'::native_trigger_service) AS \"nextcloud_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'google'::native_trigger_service) AS \"google_used!\",\n EXISTS(SELECT 1 FROM native_trigger WHERE workspace_id = $1 AND service_name = 'github'::native_trigger_service) AS \"github_used!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "websocket_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "http_routes_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "kafka_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "nats_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "postgres_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "mqtt_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "amqp_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "sqs_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "gcp_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "azure_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "email_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "nextcloud_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "google_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "github_used!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a47f7d625a96ad47277609ae173c408eda4323ec12ade20ec0da565051128f26"
|
||||
}
|
||||
+2
-1
@@ -192,7 +192,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -167,7 +167,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -47,7 +47,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -112,7 +112,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -112,7 +112,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -252,7 +252,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github",
|
||||
"data_pipeline"
|
||||
"data_pipeline",
|
||||
"trigger_amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO amqp_trigger (\n amqp_resource_path, queue_name, exchange, options, path, script_path, is_flow,\n workspace_id, edited_by, edited_at, extra_perms, server_id, last_server_ping,\n error, error_handler_path, error_handler_args, retry, mode, permissioned_as, labels\n )\n SELECT\n amqp_resource_path, queue_name, exchange, options, path, script_path, is_flow,\n $1, edited_by, edited_at, extra_perms, NULL, NULL,\n NULL, error_handler_path, error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels\n FROM amqp_trigger WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d9d997591a163f25a9be5134bffacd0068c51ca21018d6751cc1044e38c06730"
|
||||
}
|
||||
+2
-1
@@ -192,7 +192,8 @@
|
||||
"github",
|
||||
"azure",
|
||||
"asset",
|
||||
"freshness"
|
||||
"freshness",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@
|
||||
"nextcloud",
|
||||
"google",
|
||||
"github",
|
||||
"azure"
|
||||
"azure",
|
||||
"amqp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+688
-30
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -22,6 +22,7 @@ members = [
|
||||
"./windmill-trigger-kafka",
|
||||
"./windmill-trigger-postgres",
|
||||
"./windmill-trigger-mqtt",
|
||||
"./windmill-trigger-amqp",
|
||||
"./windmill-trigger-websocket",
|
||||
"./windmill-trigger-email",
|
||||
"./windmill-trigger-nats",
|
||||
@@ -141,6 +142,7 @@ postgres_trigger = ["windmill-api/postgres_trigger"]
|
||||
mcp = ["windmill-ai/mcp", "windmill-api/mcp", "windmill-worker/mcp"]
|
||||
bedrock = ["windmill-ai/bedrock", "windmill-api/bedrock", "windmill-worker/bedrock"]
|
||||
mqtt_trigger = ["windmill-api/mqtt_trigger"]
|
||||
amqp_trigger = ["windmill-api/amqp_trigger"]
|
||||
native_trigger = ["windmill-api/native_trigger"]
|
||||
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
|
||||
gcp_trigger = ["windmill-api/gcp_trigger"]
|
||||
@@ -180,7 +182,7 @@ run_inline = ["windmill-api/run_inline"]
|
||||
oss_core = [
|
||||
"embedding", "parquet", "openidconnect", "license",
|
||||
"http_trigger", "zip", "oauth2", "postgres_trigger",
|
||||
"mqtt_trigger", "websocket", "smtp", "native_trigger",
|
||||
"mqtt_trigger", "amqp_trigger", "websocket", "smtp", "native_trigger",
|
||||
"static_frontend", "mcp", "bedrock", "run_inline",
|
||||
"quickjs"
|
||||
]
|
||||
@@ -201,7 +203,7 @@ ee_rhel = ["ce_core", "ee_core", "kafka-gssapi", "all_languages"]
|
||||
ee_windows = ["ce_core", "ee_core", "all_languages_windows"]
|
||||
all_sqlx_features = ["all_languages", "enterprise", "enterprise_saml", "embedding", "parquet", "prometheus", "flow_testing",
|
||||
"openidconnect", "cloud", "jemalloc", "tantivy", "sqlx", "kafka", "kafka-gssapi", "nats", "otel", "dind", "websocket", "http_trigger",
|
||||
"postgres_trigger", "mcp", "mqtt_trigger", "sqs_trigger", "gcp_trigger", "azure_trigger", "smtp", "stripe",
|
||||
"postgres_trigger", "mcp", "mqtt_trigger", "amqp_trigger", "sqs_trigger", "gcp_trigger", "azure_trigger", "smtp", "stripe",
|
||||
"license", "oauth2", "zip", "static_frontend", "scoped_cache", "agent_worker_server", "bedrock", "native_trigger", "quickjs",
|
||||
"windmill-git-sync/all_sqlx_features"]
|
||||
|
||||
@@ -361,6 +363,7 @@ windmill-trigger = { path = "./windmill-trigger" }
|
||||
windmill-trigger-kafka = { path = "./windmill-trigger-kafka" }
|
||||
windmill-trigger-postgres = { path = "./windmill-trigger-postgres" }
|
||||
windmill-trigger-mqtt = { path = "./windmill-trigger-mqtt" }
|
||||
windmill-trigger-amqp = { path = "./windmill-trigger-amqp" }
|
||||
windmill-trigger-websocket = { path = "./windmill-trigger-websocket" }
|
||||
windmill-trigger-email = { path = "./windmill-trigger-email" }
|
||||
windmill-trigger-nats = { path = "./windmill-trigger-nats" }
|
||||
@@ -678,6 +681,9 @@ tree-sitter-ruby = "=0.23.1"
|
||||
tree-sitter-r = "=1.2.0"
|
||||
oracle = { version = "0.6.3", features = ["chrono"] }
|
||||
rumqttc = { version = "0.24.0", features = ["use-native-tls"]}
|
||||
lapin = "2.5"
|
||||
tokio-executor-trait = "2.1"
|
||||
tokio-reactor-trait = "1.1"
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
strum_macros = "0.27"
|
||||
hudsucker = { version = "0.22", features = ["rcgen-ca", "native-tls-client"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
eb3690a34b41134c91b9cb6f90b11daa541ad171
|
||||
aaa6cb89b05b76139252c64f057e53b94d12ac60
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE amqp_trigger;
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE amqp_trigger (
|
||||
amqp_resource_path VARCHAR(255) NOT NULL,
|
||||
queue_name VARCHAR(255) NOT NULL,
|
||||
exchange JSONB NULL,
|
||||
options JSONB 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,
|
||||
edited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
extra_perms JSONB NOT NULL DEFAULT '{}',
|
||||
server_id VARCHAR(50) NULL,
|
||||
last_server_ping TIMESTAMPTZ NULL,
|
||||
error TEXT NULL,
|
||||
error_handler_path VARCHAR(255) NULL,
|
||||
error_handler_args JSONB NULL,
|
||||
retry JSONB NULL,
|
||||
mode TRIGGER_MODE NOT NULL DEFAULT 'enabled'::TRIGGER_MODE,
|
||||
permissioned_as VARCHAR(255) NOT NULL,
|
||||
labels TEXT[] NULL,
|
||||
PRIMARY KEY (path, workspace_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_amqp_trigger_labels ON amqp_trigger USING gin (labels) WHERE labels IS NOT NULL;
|
||||
|
||||
GRANT ALL ON amqp_trigger TO windmill_user;
|
||||
GRANT ALL ON amqp_trigger TO windmill_admin;
|
||||
|
||||
ALTER TABLE amqp_trigger ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY admin_policy ON amqp_trigger FOR ALL TO windmill_admin USING (true);
|
||||
|
||||
CREATE POLICY see_folder_extra_perms_user_select ON amqp_trigger FOR SELECT TO windmill_user
|
||||
USING (SPLIT_PART(amqp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(amqp_trigger.path, '/', 2) = any((select regexp_split_to_array(current_setting('session.folders_read'), ','))::text[]));
|
||||
CREATE POLICY see_folder_extra_perms_user_insert ON amqp_trigger FOR INSERT TO windmill_user
|
||||
WITH CHECK (SPLIT_PART(amqp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(amqp_trigger.path, '/', 2) = any((select regexp_split_to_array(current_setting('session.folders_write'), ','))::text[]));
|
||||
CREATE POLICY see_folder_extra_perms_user_update ON amqp_trigger FOR UPDATE TO windmill_user
|
||||
USING (SPLIT_PART(amqp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(amqp_trigger.path, '/', 2) = any((select regexp_split_to_array(current_setting('session.folders_write'), ','))::text[]));
|
||||
CREATE POLICY see_folder_extra_perms_user_delete ON amqp_trigger FOR DELETE TO windmill_user
|
||||
USING (SPLIT_PART(amqp_trigger.path, '/', 1) = 'f' AND SPLIT_PART(amqp_trigger.path, '/', 2) = any((select regexp_split_to_array(current_setting('session.folders_write'), ','))::text[]));
|
||||
|
||||
CREATE POLICY see_own ON amqp_trigger FOR ALL TO windmill_user
|
||||
USING (SPLIT_PART(amqp_trigger.path, '/', 1) = 'u' AND SPLIT_PART(amqp_trigger.path, '/', 2) = (select current_setting('session.user')));
|
||||
CREATE POLICY see_member ON amqp_trigger FOR ALL TO windmill_user
|
||||
USING (SPLIT_PART(amqp_trigger.path, '/', 1) = 'g' AND SPLIT_PART(amqp_trigger.path, '/', 2) = any((select regexp_split_to_array(current_setting('session.groups'), ','))::text[]));
|
||||
|
||||
CREATE POLICY see_extra_perms_user_select ON amqp_trigger FOR SELECT TO windmill_user
|
||||
USING (extra_perms ? (select concat('u/', current_setting('session.user'))));
|
||||
CREATE POLICY see_extra_perms_user_insert ON amqp_trigger FOR INSERT TO windmill_user
|
||||
WITH CHECK ((extra_perms ->> (select concat('u/', current_setting('session.user'))))::boolean);
|
||||
CREATE POLICY see_extra_perms_user_update ON amqp_trigger FOR UPDATE TO windmill_user
|
||||
USING ((extra_perms ->> (select concat('u/', current_setting('session.user'))))::boolean);
|
||||
CREATE POLICY see_extra_perms_user_delete ON amqp_trigger FOR DELETE TO windmill_user
|
||||
USING ((extra_perms ->> (select concat('u/', current_setting('session.user'))))::boolean);
|
||||
|
||||
CREATE POLICY see_extra_perms_groups_select ON amqp_trigger FOR SELECT TO windmill_user
|
||||
USING (extra_perms ?| (select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[]);
|
||||
CREATE POLICY see_extra_perms_groups_insert ON amqp_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((select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[])
|
||||
AND value::boolean));
|
||||
CREATE POLICY see_extra_perms_groups_update ON amqp_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((select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[])
|
||||
AND value::boolean));
|
||||
CREATE POLICY see_extra_perms_groups_delete ON amqp_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((select regexp_split_to_array(current_setting('session.pgroups'), ','))::text[])
|
||||
AND value::boolean));
|
||||
|
||||
-- Enum values for the new trigger kind. ALTER TYPE ... ADD VALUE runs inside the
|
||||
-- migration transaction on PG >= 14 (Windmill's minimum) as long as the value
|
||||
-- isn't used in the same transaction — the amqp_trigger table above does not
|
||||
-- reference these enum types.
|
||||
ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'amqp';
|
||||
ALTER TYPE job_trigger_kind ADD VALUE IF NOT EXISTS 'amqp';
|
||||
ALTER TYPE draft_kind ADD VALUE IF NOT EXISTS 'trigger_amqp';
|
||||
@@ -172,6 +172,7 @@ pub enum TriggerSpec {
|
||||
Email,
|
||||
Kafka,
|
||||
Mqtt,
|
||||
Amqp,
|
||||
Nats,
|
||||
Postgres,
|
||||
Sqs,
|
||||
@@ -1527,6 +1528,7 @@ fn parse_trigger_spec(s: &str) -> Option<TriggerSpec> {
|
||||
("email", TriggerSpec::Email),
|
||||
("kafka", TriggerSpec::Kafka),
|
||||
("mqtt", TriggerSpec::Mqtt),
|
||||
("amqp", TriggerSpec::Amqp),
|
||||
("nats", TriggerSpec::Nats),
|
||||
("postgres", TriggerSpec::Postgres),
|
||||
("sqs", TriggerSpec::Sqs),
|
||||
@@ -1722,7 +1724,10 @@ mod pipeline_annotation_tests {
|
||||
);
|
||||
// A non-ASCII body must not panic on a byte offset that is not a char
|
||||
// boundary.
|
||||
assert_eq!(split_measure_filter("sum(amount) + π"), ("sum(amount) + π", None));
|
||||
assert_eq!(
|
||||
split_measure_filter("sum(amount) + π"),
|
||||
("sum(amount) + π", None)
|
||||
);
|
||||
assert_eq!(
|
||||
split_measure_filter("sum(π) where region = 'π'"),
|
||||
("sum(π)", Some("region = 'π'".to_string()))
|
||||
|
||||
@@ -121,6 +121,7 @@ fn native_str(t: &TriggerSpec) -> Option<&'static str> {
|
||||
TriggerSpec::Email => "email",
|
||||
TriggerSpec::Kafka => "kafka",
|
||||
TriggerSpec::Mqtt => "mqtt",
|
||||
TriggerSpec::Amqp => "amqp",
|
||||
TriggerSpec::Nats => "nats",
|
||||
TriggerSpec::Postgres => "postgres",
|
||||
TriggerSpec::Sqs => "sqs",
|
||||
|
||||
@@ -784,6 +784,11 @@ enum TriggerEdge {
|
||||
runnable_kind: AssetUsageKind,
|
||||
runnable_path: String,
|
||||
},
|
||||
Amqp {
|
||||
path: String,
|
||||
runnable_kind: AssetUsageKind,
|
||||
runnable_path: String,
|
||||
},
|
||||
Nats {
|
||||
path: String,
|
||||
runnable_kind: AssetUsageKind,
|
||||
@@ -928,6 +933,9 @@ async fn asset_graph(
|
||||
SELECT 'mqtt', path, script_path, is_flow FROM mqtt_trigger
|
||||
WHERE workspace_id = $1
|
||||
UNION ALL
|
||||
SELECT 'amqp', path, script_path, is_flow FROM amqp_trigger
|
||||
WHERE workspace_id = $1
|
||||
UNION ALL
|
||||
SELECT 'nats', path, script_path, is_flow FROM nats_trigger
|
||||
WHERE workspace_id = $1
|
||||
UNION ALL
|
||||
@@ -1210,6 +1218,7 @@ async fn asset_graph(
|
||||
"email" => TriggerEdge::Email { path, runnable_kind, runnable_path: script_path },
|
||||
"kafka" => TriggerEdge::Kafka { path, runnable_kind, runnable_path: script_path },
|
||||
"mqtt" => TriggerEdge::Mqtt { path, runnable_kind, runnable_path: script_path },
|
||||
"amqp" => TriggerEdge::Amqp { path, runnable_kind, runnable_path: script_path },
|
||||
"nats" => TriggerEdge::Nats { path, runnable_kind, runnable_path: script_path },
|
||||
"postgres" => TriggerEdge::Postgres { path, runnable_kind, runnable_path: script_path },
|
||||
"sqs" => TriggerEdge::Sqs { path, runnable_kind, runnable_path: script_path },
|
||||
|
||||
@@ -261,6 +261,7 @@ pub enum ScopeDomain {
|
||||
KafkaTriggers,
|
||||
NatsTriggers,
|
||||
MqttTriggers,
|
||||
AmqpTriggers,
|
||||
SqsTriggers,
|
||||
GcpTriggers,
|
||||
AzureTriggers,
|
||||
@@ -323,6 +324,7 @@ impl ScopeDomain {
|
||||
Self::KafkaTriggers => "kafka_triggers",
|
||||
Self::NatsTriggers => "nats_triggers",
|
||||
Self::MqttTriggers => "mqtt_triggers",
|
||||
Self::AmqpTriggers => "amqp_triggers",
|
||||
Self::SqsTriggers => "sqs_triggers",
|
||||
Self::GcpTriggers => "gcp_triggers",
|
||||
Self::AzureTriggers => "azure_triggers",
|
||||
@@ -378,6 +380,7 @@ impl ScopeDomain {
|
||||
"kafka_triggers" => Some(Self::KafkaTriggers),
|
||||
"nats_triggers" => Some(Self::NatsTriggers),
|
||||
"mqtt_triggers" => Some(Self::MqttTriggers),
|
||||
"amqp_triggers" => Some(Self::AmqpTriggers),
|
||||
"sqs_triggers" => Some(Self::SqsTriggers),
|
||||
"gcp_triggers" => Some(Self::GcpTriggers),
|
||||
"azure_triggers" => Some(Self::AzureTriggers),
|
||||
|
||||
@@ -46,13 +46,13 @@ fn audit_action_prefix_for_acl_kind(kind: &str) -> Option<&'static str> {
|
||||
"variable" => Some("variables"),
|
||||
"schedule" => Some("schedules"),
|
||||
"http_trigger" | "websocket_trigger" | "kafka_trigger" | "nats_trigger"
|
||||
| "postgres_trigger" | "mqtt_trigger" | "gcp_trigger" | "azure_trigger" | "sqs_trigger"
|
||||
| "email_trigger" => Some("triggers"),
|
||||
| "postgres_trigger" | "mqtt_trigger" | "amqp_trigger" | "gcp_trigger"
|
||||
| "azure_trigger" | "sqs_trigger" | "email_trigger" => Some("triggers"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const KINDS: [&str; 20] = [
|
||||
const KINDS: [&str; 21] = [
|
||||
"script",
|
||||
"group_",
|
||||
"resource",
|
||||
@@ -68,6 +68,7 @@ const KINDS: [&str; 20] = [
|
||||
"nats_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"gcp_trigger",
|
||||
"azure_trigger",
|
||||
"sqs_trigger",
|
||||
|
||||
@@ -1751,6 +1751,7 @@ pub async fn delete_workspace_user_internal(
|
||||
"kafka_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"nats_trigger",
|
||||
"sqs_trigger",
|
||||
"gcp_trigger",
|
||||
|
||||
@@ -4444,6 +4444,7 @@ struct UsedTriggers {
|
||||
pub nats_used: bool,
|
||||
pub postgres_used: bool,
|
||||
pub mqtt_used: bool,
|
||||
pub amqp_used: bool,
|
||||
pub sqs_used: bool,
|
||||
pub gcp_used: bool,
|
||||
pub azure_used: bool,
|
||||
@@ -4469,6 +4470,7 @@ 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 amqp_trigger WHERE workspace_id = $1) AS "amqp_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!",
|
||||
EXISTS(SELECT 1 FROM azure_trigger WHERE workspace_id = $1) AS "azure_used!",
|
||||
@@ -5146,6 +5148,23 @@ async fn clone_triggers_and_schedules(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO amqp_trigger (
|
||||
amqp_resource_path, queue_name, exchange, options, path, script_path, is_flow,
|
||||
workspace_id, edited_by, edited_at, extra_perms, server_id, last_server_ping,
|
||||
error, error_handler_path, error_handler_args, retry, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
amqp_resource_path, queue_name, exchange, options, path, script_path, is_flow,
|
||||
$1, edited_by, edited_at, extra_perms, NULL, NULL,
|
||||
NULL, error_handler_path, error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM amqp_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO sqs_trigger (
|
||||
path, queue_url, aws_resource_path, message_attributes, script_path,
|
||||
|
||||
@@ -279,6 +279,15 @@ pub(crate) async fn change_workspace_id(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
info!("Updating amqp_trigger table");
|
||||
sqlx::query!(
|
||||
"UPDATE amqp_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
&rw.new_id,
|
||||
&old_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
info!("Updating gcp_trigger table");
|
||||
sqlx::query!(
|
||||
"UPDATE gcp_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
|
||||
@@ -10,8 +10,8 @@ path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-api-configs/private", "windmill-api-settings/private", "windmill-api-assets/private", "windmill-api-agent-workers?/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-azure?/private", "windmill-trigger-email?/private", "windmill-git-sync/private", "windmill-autoscaling?/private", "windmill-object-store/private"]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-flows/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-schedule/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-azure?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "license"]
|
||||
private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-api-configs/private", "windmill-api-settings/private", "windmill-api-assets/private", "windmill-api-agent-workers?/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-amqp?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-azure?/private", "windmill-trigger-email?/private", "windmill-git-sync/private", "windmill-autoscaling?/private", "windmill-object-store/private"]
|
||||
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-flows/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-schedule/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-amqp?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-azure?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "license"]
|
||||
stripe = []
|
||||
run_inline = ["dep:windmill-worker", "windmill-api-configs/run_inline"]
|
||||
agent_worker_server = ["dep:windmill-worker", "dep:windmill-api-agent-workers"]
|
||||
@@ -34,6 +34,7 @@ http_trigger = ["dep:matchit", "dep:windmill-trigger-http", "windmill-store/http
|
||||
static_frontend = ["dep:rust-embed"]
|
||||
postgres_trigger = ["dep:windmill-trigger-postgres", "windmill-store/postgres_trigger"]
|
||||
mqtt_trigger = ["dep:windmill-trigger-mqtt", "windmill-store/mqtt_trigger"]
|
||||
amqp_trigger = ["dep:windmill-trigger-amqp", "windmill-store/amqp_trigger"]
|
||||
native_trigger = ["dep:windmill-native-triggers", "windmill-native-triggers/native_trigger", "dep:strum", "oauth2"]
|
||||
sqs_trigger = ["dep:windmill-trigger-sqs", "windmill-store/sqs_trigger"]
|
||||
gcp_trigger = ["dep:windmill-trigger-gcp", "windmill-store/gcp_trigger"]
|
||||
@@ -141,6 +142,7 @@ matchit = { workspace = true, optional = true }
|
||||
windmill-trigger-kafka = { workspace = true, optional = true }
|
||||
windmill-trigger-postgres = { workspace = true, optional = true }
|
||||
windmill-trigger-mqtt = { workspace = true, optional = true }
|
||||
windmill-trigger-amqp = { workspace = true, optional = true }
|
||||
windmill-trigger-websocket = { workspace = true, optional = true }
|
||||
windmill-trigger-email = { workspace = true, optional = true }
|
||||
windmill-trigger-nats = { workspace = true, optional = true }
|
||||
|
||||
@@ -5629,6 +5629,8 @@ paths:
|
||||
type: boolean
|
||||
mqtt_used:
|
||||
type: boolean
|
||||
amqp_used:
|
||||
type: boolean
|
||||
gcp_used:
|
||||
type: boolean
|
||||
azure_used:
|
||||
@@ -5650,6 +5652,7 @@ paths:
|
||||
- nats_used
|
||||
- postgres_used
|
||||
- mqtt_used
|
||||
- amqp_used
|
||||
- gcp_used
|
||||
- azure_used
|
||||
- sqs_used
|
||||
@@ -17457,6 +17460,211 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/amqp_triggers/create:
|
||||
post:
|
||||
summary: create amqp trigger
|
||||
operationId: createAmqpTrigger
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: new amqp trigger
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewAmqpTrigger"
|
||||
responses:
|
||||
"201":
|
||||
description: amqp trigger created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/amqp_triggers/update/{path}:
|
||||
post:
|
||||
summary: update amqp trigger
|
||||
operationId: updateAmqpTrigger
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
requestBody:
|
||||
description: updated trigger
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/EditAmqpTrigger"
|
||||
responses:
|
||||
"200":
|
||||
description: amqp trigger updated
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/amqp_triggers/delete/{path}:
|
||||
delete:
|
||||
summary: delete amqp trigger
|
||||
operationId: deleteAmqpTrigger
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: amqp trigger deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/amqp_triggers/get/{path}:
|
||||
get:
|
||||
summary: get amqp trigger
|
||||
operationId: getAmqpTrigger
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
- $ref: "#/components/parameters/GetDraft"
|
||||
responses:
|
||||
"200":
|
||||
description: amqp trigger retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/AmqpTrigger"
|
||||
- $ref: "#/components/schemas/UserDraftOverlay"
|
||||
|
||||
/w/{workspace}/amqp_triggers/list:
|
||||
get:
|
||||
summary: list amqp triggers
|
||||
operationId: listAmqpTriggers
|
||||
tags:
|
||||
- amqp_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
|
||||
- name: label
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: Filter by label
|
||||
- $ref: "#/components/parameters/IncludeDraftOnly"
|
||||
responses:
|
||||
"200":
|
||||
description: amqp trigger list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AmqpTrigger"
|
||||
|
||||
/w/{workspace}/amqp_triggers/exists/{path}:
|
||||
get:
|
||||
summary: does amqp trigger exists
|
||||
operationId: existsAmqpTrigger
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: amqp trigger exists
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/w/{workspace}/amqp_triggers/setmode/{path}:
|
||||
post:
|
||||
summary: set enabled amqp trigger
|
||||
operationId: setAmqpTriggerMode
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
requestBody:
|
||||
description: updated amqp trigger enable
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
$ref: "#/components/schemas/TriggerMode"
|
||||
force:
|
||||
type: boolean
|
||||
description: >
|
||||
Bypass the parent-state conflict warning when enabling a
|
||||
trigger in a fork whose parent has the same path enabled.
|
||||
required:
|
||||
- mode
|
||||
responses:
|
||||
"200":
|
||||
description: amqp trigger enabled set
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/amqp_triggers/test:
|
||||
post:
|
||||
summary: test amqp connection
|
||||
operationId: testAmqpConnection
|
||||
tags:
|
||||
- amqp_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: test amqp connection
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
amqp_resource_path:
|
||||
type: string
|
||||
description: Path to the AMQP resource containing broker connection configuration
|
||||
required:
|
||||
- amqp_resource_path
|
||||
responses:
|
||||
"200":
|
||||
description: successfully connected to amqp
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/gcp_triggers/create:
|
||||
post:
|
||||
summary: create gcp trigger
|
||||
@@ -19966,6 +20174,7 @@ paths:
|
||||
nats_trigger,
|
||||
postgres_trigger,
|
||||
mqtt_trigger,
|
||||
amqp_trigger,
|
||||
gcp_trigger,
|
||||
azure_trigger,
|
||||
sqs_trigger,
|
||||
@@ -20013,6 +20222,7 @@ paths:
|
||||
nats_trigger,
|
||||
postgres_trigger,
|
||||
mqtt_trigger,
|
||||
amqp_trigger,
|
||||
gcp_trigger,
|
||||
azure_trigger,
|
||||
sqs_trigger,
|
||||
@@ -20071,6 +20281,7 @@ paths:
|
||||
nats_trigger,
|
||||
postgres_trigger,
|
||||
mqtt_trigger,
|
||||
amqp_trigger,
|
||||
gcp_trigger,
|
||||
azure_trigger,
|
||||
sqs_trigger,
|
||||
@@ -23229,6 +23440,7 @@ components:
|
||||
- trigger_kafka
|
||||
- trigger_nats
|
||||
- trigger_mqtt
|
||||
- trigger_amqp
|
||||
- trigger_sqs
|
||||
- trigger_gcp
|
||||
- trigger_azure
|
||||
@@ -26415,6 +26627,7 @@ components:
|
||||
- kafka
|
||||
- nats
|
||||
- mqtt
|
||||
- amqp
|
||||
- sqs
|
||||
- gcp
|
||||
- azure
|
||||
@@ -26919,6 +27132,8 @@ components:
|
||||
type: number
|
||||
mqtt_count:
|
||||
type: number
|
||||
amqp_count:
|
||||
type: number
|
||||
gcp_count:
|
||||
type: number
|
||||
azure_count:
|
||||
@@ -27435,6 +27650,183 @@ components:
|
||||
- subscribe_topics
|
||||
- mqtt_resource_path
|
||||
|
||||
AmqpExchange:
|
||||
type: object
|
||||
properties:
|
||||
exchange_name:
|
||||
type: string
|
||||
description: Name of the exchange to bind the consumed queue to
|
||||
routing_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Routing keys used to bind the queue to the exchange
|
||||
required:
|
||||
- exchange_name
|
||||
|
||||
AmqpOptions:
|
||||
type: object
|
||||
properties:
|
||||
declare_queue:
|
||||
type: boolean
|
||||
description: Declare the queue (durable) before consuming; when false the queue is declared passively and must already exist
|
||||
prefetch_count:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 1
|
||||
maximum: 65535
|
||||
description: Maximum number of unacknowledged messages the broker delivers at once (1-65535)
|
||||
|
||||
AmqpTrigger:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TriggerExtraProperty"
|
||||
type: object
|
||||
properties:
|
||||
amqp_resource_path:
|
||||
type: string
|
||||
description: Path to the AMQP resource containing broker connection configuration
|
||||
queue_name:
|
||||
type: string
|
||||
description: Name of the queue to consume messages from
|
||||
exchange:
|
||||
$ref: "#/components/schemas/AmqpExchange"
|
||||
nullable: true
|
||||
description: Optional exchange binding for the consumed queue
|
||||
options:
|
||||
$ref: "#/components/schemas/AmqpOptions"
|
||||
nullable: true
|
||||
description: Optional consumer options (queue declaration, prefetch)
|
||||
server_id:
|
||||
type: string
|
||||
description: ID of the server currently handling this trigger (internal)
|
||||
last_server_ping:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp of last server heartbeat (internal)
|
||||
error:
|
||||
type: string
|
||||
description: Last error message if the trigger failed
|
||||
error_handler_path:
|
||||
type: string
|
||||
description: Path to a script or flow to run when the triggered job fails
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: Arguments to pass to the error handler
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
description: Retry configuration for failed executions
|
||||
required:
|
||||
- amqp_resource_path
|
||||
- queue_name
|
||||
|
||||
NewAmqpTrigger:
|
||||
type: object
|
||||
properties:
|
||||
amqp_resource_path:
|
||||
type: string
|
||||
description: Path to the AMQP resource containing broker connection configuration
|
||||
queue_name:
|
||||
type: string
|
||||
description: Name of the queue to consume messages from
|
||||
exchange:
|
||||
nullable: true
|
||||
$ref: "#/components/schemas/AmqpExchange"
|
||||
description: Optional exchange binding for the consumed queue
|
||||
options:
|
||||
nullable: true
|
||||
$ref: "#/components/schemas/AmqpOptions"
|
||||
description: Optional consumer options (queue declaration, prefetch)
|
||||
path:
|
||||
type: string
|
||||
description: The unique Windmill path for this trigger. Must be of the form `u/<user>/<path>` or `f/<folder>/<path>`.
|
||||
script_path:
|
||||
type: string
|
||||
description: Path to the script or flow to execute when a message is received
|
||||
is_flow:
|
||||
type: boolean
|
||||
description: True if script_path points to a flow, false if it points to a script
|
||||
mode:
|
||||
$ref: "#/components/schemas/TriggerMode"
|
||||
error_handler_path:
|
||||
type: string
|
||||
description: Path to a script or flow to run when the triggered job fails
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: Arguments to pass to the error handler
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
description: Retry configuration for failed executions
|
||||
permissioned_as:
|
||||
type: string
|
||||
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
|
||||
preserve_permissioned_as:
|
||||
type: boolean
|
||||
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
- is_flow
|
||||
- amqp_resource_path
|
||||
- queue_name
|
||||
|
||||
EditAmqpTrigger:
|
||||
type: object
|
||||
properties:
|
||||
amqp_resource_path:
|
||||
type: string
|
||||
description: Path to the AMQP resource containing broker connection configuration
|
||||
queue_name:
|
||||
type: string
|
||||
description: Name of the queue to consume messages from
|
||||
exchange:
|
||||
nullable: true
|
||||
$ref: "#/components/schemas/AmqpExchange"
|
||||
description: Optional exchange binding for the consumed queue
|
||||
options:
|
||||
nullable: true
|
||||
$ref: "#/components/schemas/AmqpOptions"
|
||||
description: Optional consumer options (queue declaration, prefetch)
|
||||
path:
|
||||
type: string
|
||||
description: The unique Windmill path for this trigger. Must be of the form `u/<user>/<path>` or `f/<folder>/<path>`.
|
||||
script_path:
|
||||
type: string
|
||||
description: Path to the script or flow to execute when a message is received
|
||||
is_flow:
|
||||
type: boolean
|
||||
description: True if script_path points to a flow, false if it points to a script
|
||||
mode:
|
||||
$ref: "#/components/schemas/TriggerMode"
|
||||
error_handler_path:
|
||||
type: string
|
||||
description: Path to a script or flow to run when the triggered job fails
|
||||
error_handler_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
description: Arguments to pass to the error handler
|
||||
retry:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
|
||||
description: Retry configuration for failed executions
|
||||
permissioned_as:
|
||||
type: string
|
||||
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
|
||||
preserve_permissioned_as:
|
||||
type: boolean
|
||||
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
- is_flow
|
||||
- amqp_resource_path
|
||||
- queue_name
|
||||
|
||||
DeliveryType:
|
||||
type: string
|
||||
enum:
|
||||
@@ -30393,6 +30785,7 @@ components:
|
||||
postgres,
|
||||
sqs,
|
||||
mqtt,
|
||||
amqp,
|
||||
gcp,
|
||||
azure,
|
||||
email,
|
||||
@@ -30575,6 +30968,7 @@ components:
|
||||
"nats_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"sqs_trigger",
|
||||
"gcp_trigger",
|
||||
"azure_trigger",
|
||||
|
||||
@@ -48,6 +48,8 @@ use windmill_common::error::Error;
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka", feature = "private"))]
|
||||
use crate::triggers::kafka::KafkaTriggerConfigConnection;
|
||||
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
use crate::triggers::amqp::{AmqpOptions, ExchangeConfig};
|
||||
#[cfg(feature = "mqtt_trigger")]
|
||||
use crate::triggers::mqtt::{MqttClientVersion, MqttV3Config, MqttV5Config, SubscribeTopic};
|
||||
|
||||
@@ -234,6 +236,14 @@ pub struct MqttTriggerConfig {
|
||||
pub client_version: Option<MqttClientVersion>,
|
||||
pub client_id: Option<String>,
|
||||
}
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AmqpTriggerConfig {
|
||||
pub amqp_resource_path: String,
|
||||
pub queue_name: String,
|
||||
pub exchange: Option<ExchangeConfig>,
|
||||
pub options: Option<AmqpOptions>,
|
||||
}
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PostgresTriggerConfig {
|
||||
@@ -271,6 +281,8 @@ enum TriggerConfig {
|
||||
Nats(NatsTriggerConfig),
|
||||
#[cfg(feature = "mqtt_trigger")]
|
||||
Mqtt(MqttTriggerConfig),
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
Amqp(AmqpTriggerConfig),
|
||||
#[cfg(all(feature = "enterprise", feature = "gcp_trigger", feature = "private"))]
|
||||
Gcp(GcpTriggerConfig),
|
||||
#[cfg(all(feature = "enterprise", feature = "azure_trigger", feature = "private"))]
|
||||
|
||||
@@ -186,6 +186,7 @@ async fn get_offboard_preview(
|
||||
"kafka_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"nats_trigger",
|
||||
"sqs_trigger",
|
||||
"gcp_trigger",
|
||||
@@ -750,6 +751,7 @@ async fn check_path_conflicts(
|
||||
"kafka_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"nats_trigger",
|
||||
"sqs_trigger",
|
||||
"gcp_trigger",
|
||||
@@ -1017,13 +1019,14 @@ async fn offboard_user_from_workspace<'c>(
|
||||
&new_permissioned_as, username, w_id
|
||||
).execute(&mut **tx).await?;
|
||||
|
||||
// ---- triggers (all 9 types with path/permissioned_as) ----
|
||||
// ---- triggers (all 10 types with path/permissioned_as) ----
|
||||
let trigger_tables = [
|
||||
"http_trigger",
|
||||
"websocket_trigger",
|
||||
"kafka_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"nats_trigger",
|
||||
"sqs_trigger",
|
||||
"gcp_trigger",
|
||||
|
||||
@@ -24,6 +24,7 @@ fn build_trigger_scope_domains() -> Vec<ScopeDomain> {
|
||||
("kafka_triggers", "Kafka"),
|
||||
("nats_triggers", "NATS"),
|
||||
("mqtt_triggers", "MQTT"),
|
||||
("amqp_triggers", "AMQP"),
|
||||
("sqs_triggers", "AWS SQS"),
|
||||
("gcp_triggers", "GCP Pub/Sub"),
|
||||
("azure_triggers", "Azure Event Grid"),
|
||||
|
||||
@@ -467,6 +467,7 @@ async fn restore_trigger(tx: &mut sqlx::PgConnection, item: &TrashItemWithData)
|
||||
"nats_trigger",
|
||||
"postgres_trigger",
|
||||
"mqtt_trigger",
|
||||
"amqp_trigger",
|
||||
"sqs_trigger",
|
||||
"gcp_trigger",
|
||||
"azure_trigger",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub use windmill_trigger_amqp::*;
|
||||
@@ -61,6 +61,16 @@ pub fn generate_trigger_routers() -> Router {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
{
|
||||
use crate::triggers::amqp::AmqpTrigger;
|
||||
|
||||
router = router.nest(
|
||||
AmqpTrigger::ROUTE_PREFIX,
|
||||
complete_trigger_routes(AmqpTrigger),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "sqs_trigger", feature = "private"))]
|
||||
{
|
||||
use crate::triggers::sqs::SqsTrigger;
|
||||
@@ -143,6 +153,7 @@ pub struct TriggersCount {
|
||||
nats_count: i64,
|
||||
postgres_count: i64,
|
||||
mqtt_count: i64,
|
||||
amqp_count: i64,
|
||||
sqs_count: i64,
|
||||
gcp_count: i64,
|
||||
azure_count: i64,
|
||||
@@ -245,6 +256,17 @@ pub async fn get_triggers_count_internal(
|
||||
#[cfg(not(feature = "mqtt_trigger"))]
|
||||
let mqtt_count = 0;
|
||||
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
let amqp_count = {
|
||||
use crate::triggers::amqp::AmqpTrigger;
|
||||
let count = AmqpTrigger
|
||||
.trigger_count(&mut tx, w_id, is_flow, path)
|
||||
.await;
|
||||
count
|
||||
};
|
||||
#[cfg(not(feature = "amqp_trigger"))]
|
||||
let amqp_count = 0;
|
||||
|
||||
#[cfg(all(feature = "sqs_trigger", feature = "enterprise", feature = "private"))]
|
||||
let sqs_count = {
|
||||
use crate::triggers::sqs::SqsTrigger;
|
||||
@@ -362,6 +384,7 @@ pub async fn get_triggers_count_internal(
|
||||
nats_count,
|
||||
postgres_count,
|
||||
mqtt_count,
|
||||
amqp_count,
|
||||
gcp_count,
|
||||
azure_count,
|
||||
sqs_count,
|
||||
|
||||
@@ -51,6 +51,14 @@ pub fn start_all_listeners(db: DB, killpill_rx: &tokio::sync::broadcast::Receive
|
||||
listen_to(MqttTrigger, db.clone(), mqtt_killpill_rx)
|
||||
}
|
||||
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
{
|
||||
let amqp_killpill_rx = killpill_rx.resubscribe();
|
||||
use crate::triggers::amqp::AmqpTrigger;
|
||||
|
||||
listen_to(AmqpTrigger, db.clone(), amqp_killpill_rx)
|
||||
}
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
{
|
||||
let mqtt_killpill_rx = killpill_rx.resubscribe();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Concrete trigger submodules (feature-gated)
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
pub mod amqp;
|
||||
#[cfg(all(feature = "azure_trigger", feature = "enterprise", feature = "private"))]
|
||||
pub mod azure;
|
||||
#[cfg(all(feature = "smtp", feature = "private"))]
|
||||
|
||||
@@ -19,6 +19,7 @@ use windmill_api_auth::check_scopes;
|
||||
feature = "websocket",
|
||||
feature = "postgres_trigger",
|
||||
feature = "mqtt_trigger",
|
||||
feature = "amqp_trigger",
|
||||
all(
|
||||
feature = "enterprise",
|
||||
any(
|
||||
@@ -144,6 +145,7 @@ pub fn is_none_or_false(val: &Option<bool>) -> bool {
|
||||
feature = "websocket",
|
||||
feature = "postgres_trigger",
|
||||
feature = "mqtt_trigger",
|
||||
feature = "amqp_trigger",
|
||||
feature = "native_trigger",
|
||||
all(
|
||||
feature = "enterprise",
|
||||
@@ -189,6 +191,7 @@ async fn fork_parent_trigger_modes(
|
||||
feature = "websocket",
|
||||
feature = "postgres_trigger",
|
||||
feature = "mqtt_trigger",
|
||||
feature = "amqp_trigger",
|
||||
feature = "native_trigger",
|
||||
all(
|
||||
feature = "enterprise",
|
||||
@@ -1226,6 +1229,36 @@ pub(crate) async fn tarball_workspace(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "amqp_trigger")]
|
||||
{
|
||||
use crate::triggers::amqp::AmqpTrigger;
|
||||
let handler = AmqpTrigger;
|
||||
let amqp_triggers = handler.list_triggers(&mut *tx, &w_id, None, None).await?;
|
||||
let parent_modes = fork_parent_trigger_modes(
|
||||
&db,
|
||||
<AmqpTrigger as TriggerCrud>::TABLE_NAME,
|
||||
parent_workspace_id.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for trigger in amqp_triggers {
|
||||
let mode_override = trigger_mode_override(&parent_modes, &trigger.base.path);
|
||||
let trigger_str = &to_string_without_metadata_inner(
|
||||
&trigger,
|
||||
ExtraPermsBehavior::Drop,
|
||||
None,
|
||||
mode_override.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
archive
|
||||
.write_to_archive(
|
||||
&trigger_str,
|
||||
&format!("{}.amqp_trigger.json", trigger.base.path),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "smtp", feature = "private"))]
|
||||
{
|
||||
use crate::triggers::email::EmailTrigger;
|
||||
|
||||
@@ -632,6 +632,7 @@ pub fn trigger_spec_to_row(spec: &TriggerSpec) -> Option<(ScriptTriggerKind, Str
|
||||
| TriggerSpec::Email
|
||||
| TriggerSpec::Kafka
|
||||
| TriggerSpec::Mqtt
|
||||
| TriggerSpec::Amqp
|
||||
| TriggerSpec::Nats
|
||||
| TriggerSpec::Postgres
|
||||
| TriggerSpec::Sqs
|
||||
|
||||
@@ -39,7 +39,8 @@ pub async fn update_triggers_script_path(
|
||||
t3 AS (UPDATE postgres_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), \
|
||||
t4 AS (UPDATE mqtt_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), \
|
||||
t5 AS (UPDATE nats_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), \
|
||||
t6 AS (UPDATE sqs_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4) \
|
||||
t6 AS (UPDATE sqs_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4), \
|
||||
t7 AS (UPDATE amqp_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4) \
|
||||
UPDATE gcp_trigger SET script_path = $1, server_id = NULL WHERE script_path = $2 AND workspace_id = $3 AND is_flow = $4",
|
||||
new_path,
|
||||
old_path,
|
||||
|
||||
@@ -45,6 +45,7 @@ pub enum UserDraftItemKind {
|
||||
TriggerKafka,
|
||||
TriggerNats,
|
||||
TriggerMqtt,
|
||||
TriggerAmqp,
|
||||
TriggerSqs,
|
||||
TriggerGcp,
|
||||
TriggerAzure,
|
||||
@@ -80,6 +81,7 @@ impl UserDraftItemKind {
|
||||
UserDraftItemKind::TriggerKafka => "trigger_kafka",
|
||||
UserDraftItemKind::TriggerNats => "trigger_nats",
|
||||
UserDraftItemKind::TriggerMqtt => "trigger_mqtt",
|
||||
UserDraftItemKind::TriggerAmqp => "trigger_amqp",
|
||||
UserDraftItemKind::TriggerSqs => "trigger_sqs",
|
||||
UserDraftItemKind::TriggerGcp => "trigger_gcp",
|
||||
UserDraftItemKind::TriggerAzure => "trigger_azure",
|
||||
@@ -94,7 +96,7 @@ impl UserDraftItemKind {
|
||||
|
||||
/// Every variant, for code that must enumerate kinds (e.g. generating
|
||||
/// the `draft_only` existence SQL).
|
||||
pub const ALL: [UserDraftItemKind; 25] = [
|
||||
pub const ALL: [UserDraftItemKind; 26] = [
|
||||
UserDraftItemKind::Script,
|
||||
UserDraftItemKind::Flow,
|
||||
UserDraftItemKind::App,
|
||||
@@ -111,6 +113,7 @@ impl UserDraftItemKind {
|
||||
UserDraftItemKind::TriggerKafka,
|
||||
UserDraftItemKind::TriggerNats,
|
||||
UserDraftItemKind::TriggerMqtt,
|
||||
UserDraftItemKind::TriggerAmqp,
|
||||
UserDraftItemKind::TriggerSqs,
|
||||
UserDraftItemKind::TriggerGcp,
|
||||
UserDraftItemKind::TriggerAzure,
|
||||
@@ -144,6 +147,7 @@ impl UserDraftItemKind {
|
||||
TriggerKafka => Some("kafka_trigger"),
|
||||
TriggerNats => Some("nats_trigger"),
|
||||
TriggerMqtt => Some("mqtt_trigger"),
|
||||
TriggerAmqp => Some("amqp_trigger"),
|
||||
TriggerSqs => Some("sqs_trigger"),
|
||||
TriggerGcp => Some("gcp_trigger"),
|
||||
TriggerAzure => Some("azure_trigger"),
|
||||
|
||||
@@ -93,6 +93,10 @@ pub enum DeployedObject {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
AmqpTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
},
|
||||
SqsTrigger {
|
||||
path: String,
|
||||
parent_path: Option<String>,
|
||||
@@ -144,6 +148,7 @@ impl DeployedObject {
|
||||
DeployedObject::NatsTrigger { path, .. } => path.to_owned(),
|
||||
DeployedObject::PostgresTrigger { path, .. } => path.to_owned(),
|
||||
DeployedObject::MqttTrigger { path, .. } => path.to_owned(),
|
||||
DeployedObject::AmqpTrigger { path, .. } => path.to_owned(),
|
||||
DeployedObject::SqsTrigger { path, .. } => path.to_owned(),
|
||||
DeployedObject::GcpTrigger { path, .. } => path.to_owned(),
|
||||
DeployedObject::AzureTrigger { path, .. } => path.to_owned(),
|
||||
@@ -186,6 +191,7 @@ impl DeployedObject {
|
||||
DeployedObject::NatsTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::PostgresTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::MqttTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::AmqpTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::SqsTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::GcpTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
DeployedObject::AzureTrigger { parent_path, .. } => parent_path.to_owned(),
|
||||
@@ -216,6 +222,7 @@ impl DeployedObject {
|
||||
DeployedObject::NatsTrigger { .. } => "nats_trigger",
|
||||
DeployedObject::PostgresTrigger { .. } => "postgres_trigger",
|
||||
DeployedObject::MqttTrigger { .. } => "mqtt_trigger",
|
||||
DeployedObject::AmqpTrigger { .. } => "amqp_trigger",
|
||||
DeployedObject::SqsTrigger { .. } => "sqs_trigger",
|
||||
DeployedObject::GcpTrigger { .. } => "gcp_trigger",
|
||||
DeployedObject::AzureTrigger { .. } => "azure_trigger",
|
||||
@@ -446,6 +453,10 @@ mod tests {
|
||||
DeployedObject::MqttTrigger { path: "t".to_string(), parent_path: None }.get_kind(),
|
||||
"mqtt_trigger"
|
||||
);
|
||||
assert_eq!(
|
||||
DeployedObject::AmqpTrigger { path: "t".to_string(), parent_path: None }.get_kind(),
|
||||
"amqp_trigger"
|
||||
);
|
||||
assert_eq!(
|
||||
DeployedObject::SqsTrigger { path: "t".to_string(), parent_path: None }.get_kind(),
|
||||
"sqs_trigger"
|
||||
|
||||
@@ -19,6 +19,7 @@ mcp = ["dep:windmill-mcp", "windmill-mcp/server", "windmill-mcp/auth"]
|
||||
http_trigger = []
|
||||
postgres_trigger = []
|
||||
mqtt_trigger = []
|
||||
amqp_trigger = []
|
||||
sqs_trigger = []
|
||||
gcp_trigger = []
|
||||
azure_trigger = []
|
||||
|
||||
@@ -2386,6 +2386,7 @@ async fn update_resource_type(
|
||||
feature = "http_trigger",
|
||||
feature = "postgres_trigger",
|
||||
feature = "mqtt_trigger",
|
||||
feature = "amqp_trigger",
|
||||
all(
|
||||
feature = "enterprise",
|
||||
any(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "windmill-trigger-amqp"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_trigger_amqp"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
enterprise = ["windmill-common/enterprise", "windmill-store/enterprise", "windmill-trigger/enterprise"]
|
||||
private = ["windmill-common/private", "windmill-store/private"]
|
||||
|
||||
[dependencies]
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-api-auth.workspace = true
|
||||
windmill-store = { workspace = true, features = ["amqp_trigger"] }
|
||||
windmill-trigger.workspace = true
|
||||
windmill-git-sync.workspace = true
|
||||
lapin.workspace = true
|
||||
tokio-executor-trait.workspace = true
|
||||
tokio-reactor-trait.workspace = true
|
||||
futures.workspace = true
|
||||
urlencoding.workspace = true
|
||||
axum.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sqlx.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
async-trait.workspace = true
|
||||
itertools.workspace = true
|
||||
base64.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -0,0 +1,203 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{types::Json as SqlxJson, PgConnection};
|
||||
use windmill_api_auth::ApiAuthed;
|
||||
use windmill_common::DB;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, Result},
|
||||
};
|
||||
use windmill_git_sync::DeployedObject;
|
||||
use windmill_store::resources::try_get_resource_from_db_as;
|
||||
use windmill_trigger::{Trigger, TriggerCrud, TriggerData};
|
||||
|
||||
use super::{
|
||||
AmqpClientBuilder, AmqpConfig, AmqpConfigRequest, AmqpOptions, AmqpResource, AmqpTrigger,
|
||||
ExchangeConfig, TestAmqpConfig,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl TriggerCrud for AmqpTrigger {
|
||||
type TriggerConfig = AmqpConfig;
|
||||
type Trigger = Trigger<Self::TriggerConfig>;
|
||||
type TriggerConfigRequest = AmqpConfigRequest;
|
||||
type TestConnectionConfig = TestAmqpConfig;
|
||||
|
||||
const TABLE_NAME: &'static str = "amqp_trigger";
|
||||
const TRIGGER_TYPE: &'static str = "amqp";
|
||||
const DRAFT_KIND: windmill_common::user_drafts::UserDraftItemKind =
|
||||
windmill_common::user_drafts::UserDraftItemKind::TriggerAmqp;
|
||||
const SUPPORTS_SERVER_STATE: bool = true;
|
||||
const SUPPORTS_TEST_CONNECTION: bool = true;
|
||||
const ROUTE_PREFIX: &'static str = "/amqp_triggers";
|
||||
const DEPLOYMENT_NAME: &'static str = "AMQP trigger";
|
||||
const ADDITIONAL_SELECT_FIELDS: &[&'static str] =
|
||||
&["amqp_resource_path", "queue_name", "exchange", "options"];
|
||||
const IS_ALLOWED_ON_CLOUD: bool = false;
|
||||
|
||||
fn get_deployed_object(path: String, parent_path: Option<String>) -> DeployedObject {
|
||||
DeployedObject::AmqpTrigger { path, parent_path }
|
||||
}
|
||||
|
||||
async fn validate_config(
|
||||
&self,
|
||||
_db: &DB,
|
||||
config: &Self::TriggerConfigRequest,
|
||||
_workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
if config.amqp_resource_path.trim().is_empty() {
|
||||
return Err(Error::BadRequest(
|
||||
"AMQP resource path cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if config.queue_name.trim().is_empty() {
|
||||
return Err(Error::BadRequest("Queue name cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
super::validate_amqp_options(config.options.as_ref()).map_err(Error::BadRequest)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_trigger(
|
||||
&self,
|
||||
_db: &DB,
|
||||
tx: &mut PgConnection,
|
||||
authed: &ApiAuthed,
|
||||
w_id: &str,
|
||||
trigger: TriggerData<Self::TriggerConfigRequest>,
|
||||
) -> Result<()> {
|
||||
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
|
||||
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
|
||||
let exchange = trigger.config.exchange.map(SqlxJson);
|
||||
let options = trigger.config.options.map(SqlxJson);
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO amqp_trigger (
|
||||
amqp_resource_path,
|
||||
queue_name,
|
||||
exchange,
|
||||
options,
|
||||
workspace_id,
|
||||
path,
|
||||
script_path,
|
||||
is_flow,
|
||||
permissioned_as,
|
||||
mode,
|
||||
edited_by,
|
||||
error_handler_path,
|
||||
error_handler_args,
|
||||
retry
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
||||
)"#,
|
||||
trigger.config.amqp_resource_path,
|
||||
trigger.config.queue_name,
|
||||
exchange as Option<SqlxJson<ExchangeConfig>>,
|
||||
options as Option<SqlxJson<AmqpOptions>>,
|
||||
w_id,
|
||||
trigger.base.path,
|
||||
trigger.base.script_path,
|
||||
trigger.base.is_flow,
|
||||
resolved_permissioned_as,
|
||||
trigger.base.mode() as _,
|
||||
&resolved_edited_by,
|
||||
trigger.error_handling.error_handler_path,
|
||||
trigger.error_handling.error_handler_args as _,
|
||||
trigger.error_handling.retry as _
|
||||
)
|
||||
.execute(tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_trigger(
|
||||
&self,
|
||||
_db: &DB,
|
||||
tx: &mut PgConnection,
|
||||
authed: &ApiAuthed,
|
||||
workspace_id: &str,
|
||||
path: &str,
|
||||
trigger: TriggerData<Self::TriggerConfigRequest>,
|
||||
) -> Result<()> {
|
||||
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
|
||||
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
|
||||
let exchange = trigger.config.exchange.map(SqlxJson);
|
||||
let options = trigger.config.options.map(SqlxJson);
|
||||
|
||||
// Important to set server_id to NULL to stop the current amqp listener
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
amqp_trigger
|
||||
SET
|
||||
amqp_resource_path = $1,
|
||||
queue_name = $2,
|
||||
exchange = $3,
|
||||
options = $4,
|
||||
is_flow = $5,
|
||||
edited_by = $6,
|
||||
permissioned_as = $7,
|
||||
script_path = $8,
|
||||
path = $9,
|
||||
edited_at = now(),
|
||||
error = NULL,
|
||||
server_id = NULL,
|
||||
error_handler_path = $12,
|
||||
error_handler_args = $13,
|
||||
retry = $14
|
||||
WHERE
|
||||
workspace_id = $10 AND
|
||||
path = $11
|
||||
"#,
|
||||
trigger.config.amqp_resource_path,
|
||||
trigger.config.queue_name,
|
||||
exchange as Option<SqlxJson<ExchangeConfig>>,
|
||||
options as Option<SqlxJson<AmqpOptions>>,
|
||||
trigger.base.is_flow,
|
||||
&resolved_edited_by,
|
||||
resolved_permissioned_as,
|
||||
trigger.base.script_path,
|
||||
trigger.base.path,
|
||||
workspace_id,
|
||||
path,
|
||||
trigger.error_handling.error_handler_path,
|
||||
trigger.error_handling.error_handler_args as _,
|
||||
trigger.error_handling.retry as _
|
||||
)
|
||||
.execute(tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_connection(
|
||||
&self,
|
||||
db: &DB,
|
||||
authed: &ApiAuthed,
|
||||
user_db: &UserDB,
|
||||
workspace_id: &str,
|
||||
config: Self::TestConnectionConfig,
|
||||
) -> Result<()> {
|
||||
let amqp_resource = try_get_resource_from_db_as::<AmqpResource>(
|
||||
authed,
|
||||
Some(user_db.clone()),
|
||||
db,
|
||||
&config.amqp_resource_path,
|
||||
workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_builder = AmqpClientBuilder::new(amqp_resource, "", None, None);
|
||||
|
||||
client_builder
|
||||
.test_connection()
|
||||
.await
|
||||
.map_err(|err| Error::BadConfig(format!("Error connecting to AMQP broker: {}", err)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
use base64::engine;
|
||||
use base64::prelude::*;
|
||||
use lapin::{
|
||||
options::{BasicConsumeOptions, BasicQosOptions, QueueBindOptions, QueueDeclareOptions},
|
||||
types::FieldTable,
|
||||
Channel, Connection, ConnectionProperties, Consumer,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::{types::Json as SqlxJson, FromRow};
|
||||
use std::collections::HashMap;
|
||||
use windmill_common::{error::Error, triggers::TriggerKind, worker::to_raw_value};
|
||||
|
||||
use windmill_trigger::trigger_helpers::TriggerJobArgs;
|
||||
|
||||
pub mod handler;
|
||||
pub mod listener;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct AmqpTrigger;
|
||||
|
||||
impl TriggerJobArgs for AmqpTrigger {
|
||||
type Payload = Vec<u8>;
|
||||
const TRIGGER_KIND: TriggerKind = TriggerKind::Amqp;
|
||||
|
||||
fn v1_payload_fn(payload: &Self::Payload) -> HashMap<String, Box<RawValue>> {
|
||||
HashMap::from([("payload".to_string(), to_raw_value(&payload))])
|
||||
}
|
||||
|
||||
fn v2_payload_fn(payload: &Self::Payload) -> HashMap<String, Box<RawValue>> {
|
||||
let base64_payload = engine::general_purpose::STANDARD.encode(payload);
|
||||
HashMap::from([("payload".to_string(), to_raw_value(&base64_payload))])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AmqpResource {
|
||||
pub host: String,
|
||||
pub port: Option<u16>,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub vhost: Option<String>,
|
||||
pub tls: Option<bool>,
|
||||
}
|
||||
|
||||
/// Binding of the consumed queue to an exchange. When present, the queue is bound
|
||||
/// to `exchange_name` for each routing key so messages published to the exchange
|
||||
/// are routed to it.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ExchangeConfig {
|
||||
pub exchange_name: String,
|
||||
#[serde(default)]
|
||||
pub routing_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct AmqpOptions {
|
||||
/// Declare the queue (durable) before consuming. When false the queue is
|
||||
/// declared passively, i.e. it must already exist on the broker.
|
||||
pub declare_queue: Option<bool>,
|
||||
/// Maximum number of unacknowledged messages the broker delivers at once.
|
||||
pub prefetch_count: Option<u16>,
|
||||
}
|
||||
|
||||
/// Shared validation for AMQP options used by both the CRUD handler and the
|
||||
/// consumer builder (which also covers capture configs, that bypass CRUD
|
||||
/// validation). RabbitMQ treats prefetch 0 as unlimited (unbounded consumer
|
||||
/// buffer), so a set prefetch must be at least 1.
|
||||
pub fn validate_amqp_options(options: Option<&AmqpOptions>) -> Result<(), String> {
|
||||
if options.and_then(|o| o.prefetch_count) == Some(0) {
|
||||
return Err("Prefetch count must be at least 1".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
pub struct AmqpConfig {
|
||||
pub amqp_resource_path: String,
|
||||
pub queue_name: String,
|
||||
pub exchange: Option<SqlxJson<ExchangeConfig>>,
|
||||
pub options: Option<SqlxJson<AmqpOptions>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AmqpConfigRequest {
|
||||
pub amqp_resource_path: String,
|
||||
pub queue_name: String,
|
||||
pub exchange: Option<ExchangeConfig>,
|
||||
pub options: Option<AmqpOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestAmqpConfig {
|
||||
pub amqp_resource_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AmqpError {
|
||||
#[error("{0}")]
|
||||
Common(#[from] Error),
|
||||
#[error("{0}")]
|
||||
Lapin(#[from] lapin::Error),
|
||||
}
|
||||
|
||||
/// Consumer bundle. The connection and channel are kept alive for as long as the
|
||||
/// consumer stream is polled: dropping them would tear down the AMQP consumer.
|
||||
pub struct AmqpConsumer {
|
||||
pub connection: Connection,
|
||||
pub channel: Channel,
|
||||
pub consumer: Consumer,
|
||||
pub queue_name: String,
|
||||
}
|
||||
|
||||
pub const CONSUMER_TAG: &str = "windmill";
|
||||
|
||||
fn build_uri(resource: &AmqpResource) -> String {
|
||||
let tls = resource.tls.unwrap_or(false);
|
||||
let scheme = if tls { "amqps" } else { "amqp" };
|
||||
let port = resource.port.unwrap_or(if tls { 5671 } else { 5672 });
|
||||
|
||||
let credentials = match (resource.username.as_deref(), resource.password.as_deref()) {
|
||||
(Some(user), password) if !user.is_empty() => format!(
|
||||
"{}:{}@",
|
||||
urlencoding::encode(user),
|
||||
urlencoding::encode(password.unwrap_or("")),
|
||||
),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
// The URI path is the virtual host and must be percent-encoded; the default
|
||||
// vhost "/" therefore becomes "%2F". An empty vhost also falls back to "/".
|
||||
let vhost = match resource.vhost.as_deref() {
|
||||
Some(v) if !v.is_empty() => v,
|
||||
_ => "/",
|
||||
};
|
||||
let vhost_encoded = urlencoding::encode(vhost);
|
||||
|
||||
// Bracket an IPv6 literal host so `host:port` parses correctly.
|
||||
let host = if resource.host.contains(':') && !resource.host.starts_with('[') {
|
||||
format!("[{}]", resource.host)
|
||||
} else {
|
||||
resource.host.clone()
|
||||
};
|
||||
|
||||
format!(
|
||||
"{}://{}{}:{}/{}",
|
||||
scheme, credentials, host, port, vhost_encoded
|
||||
)
|
||||
}
|
||||
|
||||
fn connection_properties() -> ConnectionProperties {
|
||||
ConnectionProperties::default()
|
||||
.with_executor(tokio_executor_trait::Tokio::current())
|
||||
.with_reactor(tokio_reactor_trait::Tokio)
|
||||
}
|
||||
|
||||
pub struct AmqpClientBuilder<'client> {
|
||||
resource: AmqpResource,
|
||||
queue_name: &'client str,
|
||||
exchange: Option<&'client ExchangeConfig>,
|
||||
options: Option<&'client AmqpOptions>,
|
||||
}
|
||||
|
||||
impl<'client> AmqpClientBuilder<'client> {
|
||||
pub fn new(
|
||||
resource: AmqpResource,
|
||||
queue_name: &'client str,
|
||||
exchange: Option<&'client ExchangeConfig>,
|
||||
options: Option<&'client AmqpOptions>,
|
||||
) -> Self {
|
||||
Self { resource, queue_name, exchange, options }
|
||||
}
|
||||
|
||||
async fn connect(&self) -> Result<Connection, AmqpError> {
|
||||
let uri = build_uri(&self.resource);
|
||||
let connection = Connection::connect(&uri, connection_properties()).await?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
/// Establish a connection and open a channel to verify the broker is reachable
|
||||
/// with the provided credentials.
|
||||
pub async fn test_connection(&self) -> Result<(), AmqpError> {
|
||||
let connection = self.connect().await?;
|
||||
connection.create_channel().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn build_consumer(&self) -> Result<AmqpConsumer, AmqpError> {
|
||||
let connection = self.connect().await?;
|
||||
let channel = connection.create_channel().await?;
|
||||
|
||||
validate_amqp_options(self.options).map_err(|e| AmqpError::Common(Error::BadConfig(e)))?;
|
||||
if let Some(prefetch_count) = self.options.and_then(|o| o.prefetch_count) {
|
||||
channel
|
||||
.basic_qos(prefetch_count, BasicQosOptions::default())
|
||||
.await?;
|
||||
}
|
||||
|
||||
let declare_queue = self.options.and_then(|o| o.declare_queue).unwrap_or(true);
|
||||
|
||||
let queue_declare_options = QueueDeclareOptions {
|
||||
passive: !declare_queue,
|
||||
durable: declare_queue,
|
||||
exclusive: false,
|
||||
auto_delete: false,
|
||||
nowait: false,
|
||||
};
|
||||
|
||||
channel
|
||||
.queue_declare(
|
||||
self.queue_name,
|
||||
queue_declare_options,
|
||||
FieldTable::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(exchange) = self.exchange {
|
||||
if !exchange.exchange_name.trim().is_empty() {
|
||||
// Bind the queue for every routing key; an empty list binds once
|
||||
// with an empty routing key (fanout exchanges ignore it anyway).
|
||||
let routing_keys = if exchange.routing_keys.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
exchange.routing_keys.clone()
|
||||
};
|
||||
for routing_key in routing_keys {
|
||||
channel
|
||||
.queue_bind(
|
||||
self.queue_name,
|
||||
&exchange.exchange_name,
|
||||
&routing_key,
|
||||
QueueBindOptions::default(),
|
||||
FieldTable::default(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let consumer = channel
|
||||
.basic_consume(
|
||||
self.queue_name,
|
||||
CONSUMER_TAG,
|
||||
BasicConsumeOptions::default(),
|
||||
FieldTable::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(AmqpConsumer { connection, channel, consumer, queue_name: self.queue_name.to_string() })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn resource(
|
||||
host: &str,
|
||||
port: Option<u16>,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
vhost: Option<&str>,
|
||||
tls: Option<bool>,
|
||||
) -> AmqpResource {
|
||||
AmqpResource {
|
||||
host: host.to_string(),
|
||||
port,
|
||||
username: username.map(str::to_string),
|
||||
password: password.map(str::to_string),
|
||||
vhost: vhost.map(str::to_string),
|
||||
tls,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_uri_defaults_encode_root_vhost_and_pick_plaintext_port() {
|
||||
let uri = build_uri(&resource("broker", None, None, None, None, None));
|
||||
assert_eq!(uri, "amqp://broker:5672/%2F");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_uri_tls_picks_amqps_and_5671() {
|
||||
let uri = build_uri(&resource("broker", None, None, None, None, Some(true)));
|
||||
assert_eq!(uri, "amqps://broker:5671/%2F");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_uri_encodes_credentials_and_custom_vhost() {
|
||||
let uri = build_uri(&resource(
|
||||
"broker",
|
||||
Some(5673),
|
||||
Some("us er"),
|
||||
Some("p@ss/word"),
|
||||
Some("my/vhost"),
|
||||
None,
|
||||
));
|
||||
assert_eq!(uri, "amqp://us%20er:p%40ss%2Fword@broker:5673/my%2Fvhost");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_uri_omits_credentials_when_username_empty() {
|
||||
let uri = build_uri(&resource(
|
||||
"broker",
|
||||
None,
|
||||
Some(""),
|
||||
Some("secret"),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
assert_eq!(uri, "amqp://broker:5672/%2F");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_uri_blank_vhost_falls_back_to_root() {
|
||||
let uri = build_uri(&resource("broker", None, None, None, Some(""), None));
|
||||
assert_eq!(uri, "amqp://broker:5672/%2F");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_uri_brackets_ipv6_host() {
|
||||
let uri = build_uri(&resource("::1", Some(5672), None, None, None, None));
|
||||
assert_eq!(uri, "amqp://[::1]:5672/%2F");
|
||||
}
|
||||
|
||||
fn options(prefetch: Option<u16>) -> AmqpOptions {
|
||||
AmqpOptions { declare_queue: None, prefetch_count: prefetch }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_amqp_options_rejects_zero_prefetch() {
|
||||
assert!(validate_amqp_options(Some(&options(Some(0)))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_amqp_options_accepts_valid_prefetch_and_none() {
|
||||
assert!(validate_amqp_options(Some(&options(Some(1)))).is_ok());
|
||||
assert!(validate_amqp_options(Some(&options(Some(65535)))).is_ok());
|
||||
assert!(validate_amqp_options(Some(&options(None))).is_ok());
|
||||
assert!(validate_amqp_options(None).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use lapin::options::{BasicAckOptions, BasicNackOptions};
|
||||
use tokio::sync::RwLock;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, Result},
|
||||
jobs::JobTriggerKind,
|
||||
utils::{report_critical_error, report_recovered_critical_error},
|
||||
worker::to_raw_value,
|
||||
DB,
|
||||
};
|
||||
|
||||
use windmill_store::resources::try_get_resource_from_db_as;
|
||||
use windmill_trigger::listener::ListeningTrigger;
|
||||
use windmill_trigger::trigger_helpers::TriggerJobArgs;
|
||||
use windmill_trigger::Listener;
|
||||
|
||||
use super::{AmqpClientBuilder, AmqpConfig, AmqpConsumer, AmqpResource, AmqpTrigger};
|
||||
|
||||
// lapin (like rdkafka) has no transparent reconnect, so — mirroring the Kafka
|
||||
// trigger — the connection is (re)established in `consume` with a backoff retry
|
||||
// loop rather than disabling the trigger on a transient broker outage.
|
||||
const RECONNECT_BACKOFF_SECS: u64 = 30;
|
||||
// Back off after a failed dispatch so a poison message that always fails can't
|
||||
// spin a tight redelivery loop; the connection stays up for other messages.
|
||||
const DISPATCH_FAILURE_BACKOFF_SECS: u64 = 5;
|
||||
|
||||
impl AmqpTrigger {
|
||||
async fn build_amqp_consumer(
|
||||
&self,
|
||||
db: &DB,
|
||||
listening_trigger: &ListeningTrigger<AmqpConfig>,
|
||||
) -> Result<AmqpConsumer> {
|
||||
let AmqpConfig { amqp_resource_path, queue_name, exchange, options } =
|
||||
&listening_trigger.trigger_config;
|
||||
|
||||
let authed = listening_trigger
|
||||
.authed(db, &Self::TRIGGER_KIND.to_string())
|
||||
.await?;
|
||||
|
||||
let amqp_resource = try_get_resource_from_db_as::<AmqpResource>(
|
||||
&authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
db,
|
||||
amqp_resource_path,
|
||||
&listening_trigger.workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client_builder = AmqpClientBuilder::new(
|
||||
amqp_resource,
|
||||
queue_name,
|
||||
exchange.as_ref().map(|e| &e.0),
|
||||
options.as_ref().map(|o| &o.0),
|
||||
);
|
||||
|
||||
client_builder
|
||||
.build_consumer()
|
||||
.await
|
||||
.map_err(|e| Error::BadConfig(format!("Failed to build AMQP consumer: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Listener for AmqpTrigger {
|
||||
type Consumer = ();
|
||||
type Extra = ();
|
||||
type ExtraState = ();
|
||||
const JOB_TRIGGER_KIND: JobTriggerKind = JobTriggerKind::Amqp;
|
||||
|
||||
async fn get_consumer(
|
||||
&self,
|
||||
_db: &DB,
|
||||
_listening_trigger: &ListeningTrigger<Self::TriggerConfig>,
|
||||
_err_message: Arc<RwLock<Option<String>>>,
|
||||
_killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> Result<Option<Self::Consumer>> {
|
||||
// The connection is established (and re-established) in `consume` so that
|
||||
// a transient broker outage retries with backoff instead of disabling the
|
||||
// trigger — mirroring the Kafka trigger, whose client also lacks a
|
||||
// transparent reconnect.
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
async fn consume(
|
||||
&self,
|
||||
db: &DB,
|
||||
_consumer: Self::Consumer,
|
||||
listening_trigger: &ListeningTrigger<Self::TriggerConfig>,
|
||||
err_message: Arc<RwLock<Option<String>>>,
|
||||
_killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
_extra_state: Option<&Self::ExtraState>,
|
||||
) {
|
||||
let path = &listening_trigger.path;
|
||||
let workspace_id = &listening_trigger.workspace_id;
|
||||
let alert_id = format!("amqp_trigger:{}", path);
|
||||
let mut tries = 0_usize;
|
||||
|
||||
// (Re)connect loop: retries forever with backoff; the framework's
|
||||
// `select!` around `consume` cancels it on killpill, and
|
||||
// `update_ping_and_loop_ping_status` returning None (trigger removed /
|
||||
// disabled / capture stopped) breaks us out.
|
||||
loop {
|
||||
let mut consumer = match self.build_amqp_consumer(db, listening_trigger).await {
|
||||
Ok(consumer) => consumer,
|
||||
Err(e) => {
|
||||
let status = format!(
|
||||
"Failed to connect (attempt {}), retrying in {}s: {}",
|
||||
tries + 1,
|
||||
RECONNECT_BACKOFF_SECS,
|
||||
e
|
||||
);
|
||||
if self
|
||||
.update_ping_and_loop_ping_status(
|
||||
db,
|
||||
listening_trigger,
|
||||
err_message.clone(),
|
||||
Some(status),
|
||||
)
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
tracing::error!(
|
||||
"AMQP trigger {} failed to connect (attempt {}): {}",
|
||||
path,
|
||||
tries + 1,
|
||||
e
|
||||
);
|
||||
if tries % 10 == 0 && listening_trigger.trigger_mode {
|
||||
report_critical_error(
|
||||
format!(
|
||||
"Failed to connect AMQP trigger {} (attempt {}), retrying every {}s. This alert repeats every 10 failed attempts. Error: {}",
|
||||
path, tries + 1, RECONNECT_BACKOFF_SECS, e
|
||||
),
|
||||
db.clone(),
|
||||
Some(workspace_id),
|
||||
Some(&alert_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
tries += 1;
|
||||
tokio::time::sleep(Duration::from_secs(RECONNECT_BACKOFF_SECS)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Connected: clear any "reconnecting" status.
|
||||
if self
|
||||
.update_ping_and_loop_ping_status(db, listening_trigger, err_message.clone(), None)
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if tries > 0 {
|
||||
tracing::info!("AMQP trigger {} reconnected after {} attempts", path, tries);
|
||||
if listening_trigger.trigger_mode {
|
||||
report_recovered_critical_error(
|
||||
format!("AMQP trigger {} reconnected", path),
|
||||
db.clone(),
|
||||
Some(workspace_id),
|
||||
Some(&alert_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
tries = 0;
|
||||
}
|
||||
|
||||
// Consume until the stream errors, then break out to reconnect.
|
||||
loop {
|
||||
match consumer.consumer.next().await {
|
||||
Some(Ok(delivery)) => {
|
||||
let trigger_info = HashMap::from([
|
||||
(
|
||||
"exchange".to_string(),
|
||||
to_raw_value(&delivery.exchange.as_str()),
|
||||
),
|
||||
(
|
||||
"routing_key".to_string(),
|
||||
to_raw_value(&delivery.routing_key.as_str()),
|
||||
),
|
||||
("queue_name".to_string(), to_raw_value(&consumer.queue_name)),
|
||||
(
|
||||
"redelivered".to_string(),
|
||||
to_raw_value(&delivery.redelivered),
|
||||
),
|
||||
(
|
||||
"delivery_tag".to_string(),
|
||||
to_raw_value(&delivery.delivery_tag),
|
||||
),
|
||||
]);
|
||||
|
||||
let dispatched = self
|
||||
.handle_event(
|
||||
db,
|
||||
listening_trigger,
|
||||
delivery.data.clone(),
|
||||
trigger_info,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Only ack once the job/capture was dispatched. On failure
|
||||
// nack with requeue so the broker redelivers rather than
|
||||
// dropping the message (at-least-once).
|
||||
let dispatch_failed = dispatched.is_err();
|
||||
let ack_result = if dispatch_failed {
|
||||
delivery
|
||||
.acker
|
||||
.nack(BasicNackOptions { requeue: true, multiple: false })
|
||||
.await
|
||||
} else {
|
||||
delivery.acker.ack(BasicAckOptions::default()).await
|
||||
};
|
||||
|
||||
if let Err(err) = ack_result {
|
||||
// Channel is gone; break out to reconnect.
|
||||
tracing::warn!(
|
||||
"AMQP trigger {} ack/nack failed, reconnecting: {}",
|
||||
path,
|
||||
err
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if dispatch_failed {
|
||||
// The message was requeued: back off before consuming
|
||||
// again so a poison message can't spin a tight
|
||||
// redelivery loop, while keeping the connection alive.
|
||||
tokio::time::sleep(Duration::from_secs(DISPATCH_FAILURE_BACKOFF_SECS))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
tracing::warn!(
|
||||
"AMQP trigger {} consumer error, reconnecting: {}",
|
||||
path,
|
||||
err
|
||||
);
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("AMQP trigger {} consumer stream ended, reconnecting", path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ fn runnable_format_from_schema_without_preprocessor(
|
||||
schema: Option<sqlx::types::Json<PartialSchema>>,
|
||||
) -> RunnableFormat {
|
||||
match trigger_kind {
|
||||
TriggerKind::Mqtt
|
||||
TriggerKind::Mqtt | TriggerKind::Amqp
|
||||
if schema.as_ref().is_some_and(|schema| {
|
||||
schema.properties.as_ref().is_some_and(|properties| {
|
||||
properties.iter().any(|(key, def)| {
|
||||
|
||||
@@ -32,6 +32,7 @@ pub enum JobTriggerKind {
|
||||
Email,
|
||||
Nats,
|
||||
Mqtt,
|
||||
Amqp,
|
||||
Sqs,
|
||||
Postgres,
|
||||
Schedule,
|
||||
@@ -66,6 +67,7 @@ impl std::fmt::Display for JobTriggerKind {
|
||||
JobTriggerKind::Email => "email",
|
||||
JobTriggerKind::Nats => "nats",
|
||||
JobTriggerKind::Mqtt => "mqtt",
|
||||
JobTriggerKind::Amqp => "amqp",
|
||||
JobTriggerKind::Sqs => "sqs",
|
||||
JobTriggerKind::Postgres => "postgres",
|
||||
JobTriggerKind::Schedule => "schedule",
|
||||
|
||||
@@ -16,6 +16,7 @@ pub enum TriggerKind {
|
||||
Email,
|
||||
Nats,
|
||||
Mqtt,
|
||||
Amqp,
|
||||
Sqs,
|
||||
Postgres,
|
||||
Gcp,
|
||||
@@ -36,6 +37,7 @@ impl TriggerKind {
|
||||
TriggerKind::DefaultEmail => "email".to_string(),
|
||||
TriggerKind::Nats => "nats".to_string(),
|
||||
TriggerKind::Mqtt => "mqtt".to_string(),
|
||||
TriggerKind::Amqp => "amqp".to_string(),
|
||||
TriggerKind::Sqs => "sqs".to_string(),
|
||||
TriggerKind::Postgres => "postgres".to_string(),
|
||||
TriggerKind::Gcp => "gcp".to_string(),
|
||||
@@ -58,6 +60,7 @@ impl fmt::Display for TriggerKind {
|
||||
TriggerKind::DefaultEmail => "default_email",
|
||||
TriggerKind::Nats => "nats",
|
||||
TriggerKind::Mqtt => "mqtt",
|
||||
TriggerKind::Amqp => "amqp",
|
||||
TriggerKind::Sqs => "sqs",
|
||||
TriggerKind::Postgres => "postgres",
|
||||
TriggerKind::Gcp => "gcp",
|
||||
|
||||
@@ -71,6 +71,7 @@ const assetNodeId = (kind: string, path: string): string => `${kind}:${path}`;
|
||||
const NON_AUTORUN_TRIGGER_KINDS = new Set([
|
||||
"kafka",
|
||||
"mqtt",
|
||||
"amqp",
|
||||
"nats",
|
||||
"postgres",
|
||||
"sqs",
|
||||
|
||||
@@ -259,6 +259,7 @@ const NATIVE_KINDS = new Set([
|
||||
"email",
|
||||
"kafka",
|
||||
"mqtt",
|
||||
"amqp",
|
||||
"nats",
|
||||
"postgres",
|
||||
"sqs",
|
||||
|
||||
@@ -1717,6 +1717,7 @@ export async function elementsToMap(
|
||||
path.endsWith(".nats_trigger" + ext) ||
|
||||
path.endsWith(".postgres_trigger" + ext) ||
|
||||
path.endsWith(".mqtt_trigger" + ext) ||
|
||||
path.endsWith(".amqp_trigger" + ext) ||
|
||||
path.endsWith(".sqs_trigger" + ext) ||
|
||||
path.endsWith(".gcp_trigger" + ext) ||
|
||||
path.endsWith(".azure_trigger" + ext) ||
|
||||
@@ -2470,6 +2471,7 @@ function getOrderFromPath(p: string) {
|
||||
typ == "nats_trigger" ||
|
||||
typ == "postgres_trigger" ||
|
||||
typ == "mqtt_trigger" ||
|
||||
typ == "amqp_trigger" ||
|
||||
typ == "sqs_trigger" ||
|
||||
typ == "gcp_trigger" ||
|
||||
typ == "azure_trigger" ||
|
||||
@@ -4237,7 +4239,7 @@ export async function push(
|
||||
}
|
||||
}
|
||||
const rules = folderRulesCache.get(folderName)!;
|
||||
const remotePath = change.path.replace(/\.(script|schedule|http_trigger|websocket_trigger|kafka_trigger|nats_trigger|postgres_trigger|mqtt_trigger|sqs_trigger|gcp_trigger|azure_trigger|email_trigger)\.(yaml|json)$/, "").replace(/(\.flow|__flow)\/flow\.(yaml|json)$/, "").replace(/\.(app|raw_app)(\/app\.(yaml|json))?$/, "");
|
||||
const remotePath = change.path.replace(/\.(script|schedule|http_trigger|websocket_trigger|kafka_trigger|nats_trigger|postgres_trigger|mqtt_trigger|amqp_trigger|sqs_trigger|gcp_trigger|azure_trigger|email_trigger)\.(yaml|json)$/, "").replace(/(\.flow|__flow)\/flow\.(yaml|json)$/, "").replace(/\.(app|raw_app)(\/app\.(yaml|json))?$/, "");
|
||||
const relative = remotePath.slice(`f/${folderName}/`.length);
|
||||
if (!relative) continue;
|
||||
for (const rule of rules) {
|
||||
@@ -4931,6 +4933,12 @@ export async function push(
|
||||
path: removeSuffix(target, ".mqtt_trigger.json"),
|
||||
});
|
||||
break;
|
||||
case "amqp_trigger":
|
||||
await wmill.deleteAmqpTrigger({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".amqp_trigger.json"),
|
||||
});
|
||||
break;
|
||||
case "sqs_trigger":
|
||||
await wmill.deleteSqsTrigger({
|
||||
workspace: workspaceId,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
HttpTrigger,
|
||||
KafkaTrigger,
|
||||
MqttTrigger,
|
||||
AmqpTrigger,
|
||||
NatsTrigger,
|
||||
PostgresTrigger,
|
||||
SqsTrigger,
|
||||
@@ -48,6 +49,7 @@ type Trigger = {
|
||||
nats: NatsTrigger;
|
||||
postgres: PostgresTrigger;
|
||||
mqtt: MqttTrigger;
|
||||
amqp: AmqpTrigger;
|
||||
sqs: SqsTrigger;
|
||||
gcp: GcpTrigger;
|
||||
azure: AzureTrigger;
|
||||
@@ -84,6 +86,7 @@ async function getTrigger<K extends TriggerType>(
|
||||
nats: wmill.getNatsTrigger,
|
||||
postgres: wmill.getPostgresTrigger,
|
||||
mqtt: wmill.getMqttTrigger,
|
||||
amqp: wmill.getAmqpTrigger,
|
||||
sqs: wmill.getSqsTrigger,
|
||||
gcp: wmill.getGcpTrigger,
|
||||
azure: wmill.getAzureTrigger,
|
||||
@@ -114,6 +117,7 @@ async function updateTrigger<K extends TriggerType>(
|
||||
nats: wmill.updateNatsTrigger,
|
||||
postgres: wmill.updatePostgresTrigger,
|
||||
mqtt: wmill.updateMqttTrigger,
|
||||
amqp: wmill.updateAmqpTrigger,
|
||||
sqs: wmill.updateSqsTrigger,
|
||||
gcp: wmill.updateGcpTrigger,
|
||||
azure: wmill.updateAzureTrigger,
|
||||
@@ -142,6 +146,7 @@ async function createTrigger<K extends TriggerType>(
|
||||
nats: wmill.createNatsTrigger,
|
||||
postgres: wmill.createPostgresTrigger,
|
||||
mqtt: wmill.createMqttTrigger,
|
||||
amqp: wmill.createAmqpTrigger,
|
||||
sqs: wmill.createSqsTrigger,
|
||||
gcp: wmill.createGcpTrigger,
|
||||
azure: wmill.createAzureTrigger,
|
||||
@@ -381,6 +386,13 @@ const triggerTemplates: Record<TriggerType, Record<string, any>> = {
|
||||
subscribe_topics: [],
|
||||
enabled: false,
|
||||
},
|
||||
amqp: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
amqp_resource_path: "",
|
||||
queue_name: "",
|
||||
enabled: false,
|
||||
},
|
||||
sqs: {
|
||||
script_path: "",
|
||||
is_flow: false,
|
||||
@@ -536,6 +548,7 @@ async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
natsTriggers,
|
||||
postgresTriggers,
|
||||
mqttTriggers,
|
||||
amqpTriggers,
|
||||
sqsTriggers,
|
||||
gcpTriggers,
|
||||
azureTriggers,
|
||||
@@ -547,6 +560,7 @@ async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
listOrEmpty(() => wmill.listNatsTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listPostgresTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listMqttTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listAmqpTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listSqsTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listGcpTriggers({ workspace: ws })),
|
||||
listOrEmpty(() => wmill.listAzureTriggers({ workspace: ws })),
|
||||
@@ -559,6 +573,7 @@ async function list(opts: GlobalOptions & { json?: boolean }) {
|
||||
...natsTriggers.map((x) => ({ path: x.path, kind: "nats" })),
|
||||
...postgresTriggers.map((x) => ({ path: x.path, kind: "postgres" })),
|
||||
...mqttTriggers.map((x) => ({ path: x.path, kind: "mqtt" })),
|
||||
...amqpTriggers.map((x) => ({ path: x.path, kind: "amqp" })),
|
||||
...sqsTriggers.map((x) => ({ path: x.path, kind: "sqs" })),
|
||||
...gcpTriggers.map((x) => ({ path: x.path, kind: "gcp" })),
|
||||
...azureTriggers.map((x) => ({ path: x.path, kind: "azure" })),
|
||||
@@ -643,11 +658,11 @@ const command = new Command()
|
||||
.command("get", "get a trigger's details")
|
||||
.arguments("<path:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--kind <kind:string>", "Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, azure, email). Recommended for faster lookup")
|
||||
.option("--kind <kind:string>", "Trigger kind (http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email). Recommended for faster lookup")
|
||||
.action(get as any)
|
||||
.command("new", "create a new trigger locally")
|
||||
.arguments("<path:string>")
|
||||
.option("--kind <kind:string>", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, azure, email)")
|
||||
.option("--kind <kind:string>", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email)")
|
||||
.action(newTrigger as any)
|
||||
.command(
|
||||
"push",
|
||||
@@ -662,7 +677,7 @@ const command = new Command()
|
||||
.arguments("<path:string> <email:string>")
|
||||
.option(
|
||||
"--kind <kind:string>",
|
||||
"Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, azure, email)"
|
||||
"Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email)"
|
||||
)
|
||||
.action((async (opts: any, triggerPath: string, email: string) => {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
|
||||
@@ -89,7 +89,7 @@ When a new app needs to be created, YOU run \`wmill app new\` yourself with \`--
|
||||
|
||||
## Triggers
|
||||
|
||||
You MUST use the \`triggers\` skill to configure HTTP routes, WebSocket, Kafka, NATS, SQS, MQTT, GCP, Azure, Email, or Postgres CDC triggers.
|
||||
You MUST use the \`triggers\` skill to configure HTTP routes, WebSocket, Kafka, NATS, SQS, MQTT, AMQP, GCP, Azure, Email, or Postgres CDC triggers.
|
||||
|
||||
## Schedules
|
||||
|
||||
|
||||
@@ -7232,12 +7232,12 @@ trigger related commands
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`trigger get <path:string>\` - get a trigger's details
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--kind <kind:string>\` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, azure, email). Recommended for faster lookup
|
||||
- \`--kind <kind:string>\` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email). Recommended for faster lookup
|
||||
- \`trigger new <path:string>\` - create a new trigger locally
|
||||
- \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, azure, email)
|
||||
- \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email)
|
||||
- \`trigger push <file_path:string> <remote_path:string>\` - push a local trigger spec. This overrides any remote versions.
|
||||
- \`trigger set-permissioned-as <path:string> <email:string>\` - Set the email (run-as user) for a trigger (requires admin or wm_deployers group)
|
||||
- \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, azure, email)
|
||||
- \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email)
|
||||
|
||||
### user
|
||||
|
||||
@@ -7509,6 +7509,120 @@ Both print the job result, are safe to run yourself, and don't deploy.
|
||||
|
||||
// YAML schema content for triggers and schedules
|
||||
export const SCHEMAS: Record<string, string> = {
|
||||
"amqp_trigger": `type: object
|
||||
properties:
|
||||
script_path:
|
||||
type: string
|
||||
description: Path to the script or flow to execute when triggered
|
||||
permissioned_as:
|
||||
type: string
|
||||
description: The user or group this trigger runs as (permissioned_as)
|
||||
is_flow:
|
||||
type: boolean
|
||||
description: True if script_path points to a flow, false if it points to a script
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
draft_only:
|
||||
type: boolean
|
||||
description: 'True when this row is a per-user draft with no deployed
|
||||
|
||||
trigger at the same path. Set by list endpoints when
|
||||
|
||||
\`include_draft_only=true\` synthesizes the row from the
|
||||
|
||||
draft. Frontend renders a "Draft" badge.
|
||||
|
||||
'
|
||||
is_draft:
|
||||
type: boolean
|
||||
description: 'True when the authed user has a per-user draft at this path
|
||||
|
||||
(over a deployed row or a synthesized draft-only row).
|
||||
|
||||
Frontend appends a \`*\` to the displayed name.
|
||||
|
||||
'
|
||||
amqp_resource_path:
|
||||
type: string
|
||||
description: Path to the AMQP resource containing broker connection configuration
|
||||
queue_name:
|
||||
type: string
|
||||
description: Name of the queue to consume messages from
|
||||
exchange:
|
||||
type: object
|
||||
properties:
|
||||
exchange_name:
|
||||
type: string
|
||||
description: Name of the exchange to bind the consumed queue to
|
||||
routing_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Routing keys used to bind the queue to the exchange
|
||||
options:
|
||||
type: object
|
||||
properties:
|
||||
declare_queue:
|
||||
type: boolean
|
||||
description: Declare the queue (durable) before consuming; when false the
|
||||
queue is declared passively and must already exist
|
||||
prefetch_count:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 1
|
||||
maximum: 65535
|
||||
description: Maximum number of unacknowledged messages the broker delivers
|
||||
at once (1-65535)
|
||||
error_handler_path:
|
||||
type: string
|
||||
description: Path to a script or flow to run when the triggered job fails
|
||||
error_handler_args:
|
||||
type: object
|
||||
description: The arguments to pass to the script or flow
|
||||
retry:
|
||||
type: object
|
||||
properties:
|
||||
constant:
|
||||
type: object
|
||||
description: Retry with constant delay between attempts
|
||||
properties:
|
||||
attempts:
|
||||
type: integer
|
||||
description: Number of retry attempts
|
||||
seconds:
|
||||
type: integer
|
||||
description: Seconds to wait between retries
|
||||
exponential:
|
||||
type: object
|
||||
description: Retry with exponential backoff (delay doubles each time)
|
||||
properties:
|
||||
attempts:
|
||||
type: integer
|
||||
description: Number of retry attempts
|
||||
multiplier:
|
||||
type: integer
|
||||
description: Multiplier for exponential backoff
|
||||
seconds:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Initial delay in seconds
|
||||
random_factor:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Random jitter percentage (0-100) to avoid thundering herd
|
||||
retry_if:
|
||||
$ref: '#/components/schemas/RetryIf'
|
||||
description: Retry configuration for failed module executions
|
||||
required:
|
||||
- script_path
|
||||
- permissioned_as
|
||||
- is_flow
|
||||
- amqp_resource_path
|
||||
- queue_name
|
||||
`,
|
||||
"azure_trigger": `type: object
|
||||
properties:
|
||||
script_path:
|
||||
@@ -8815,6 +8929,7 @@ export const SCHEMA_MAPPINGS: Record<string, SchemaMapping[]> = {
|
||||
{ name: "NatsTrigger", schemaKey: "nats_trigger", filePattern: "*.nats_trigger.yaml" },
|
||||
{ name: "PostgresTrigger", schemaKey: "postgres_trigger", filePattern: "*.postgres_trigger.yaml" },
|
||||
{ name: "MqttTrigger", schemaKey: "mqtt_trigger", filePattern: "*.mqtt_trigger.yaml" },
|
||||
{ name: "AmqpTrigger", schemaKey: "amqp_trigger", filePattern: "*.amqp_trigger.yaml" },
|
||||
{ name: "SqsTrigger", schemaKey: "sqs_trigger", filePattern: "*.sqs_trigger.yaml" },
|
||||
{ name: "GcpTrigger", schemaKey: "gcp_trigger", filePattern: "*.gcp_trigger.yaml" },
|
||||
{ name: "AzureTrigger", schemaKey: "azure_trigger", filePattern: "*.azure_trigger.yaml" },
|
||||
|
||||
@@ -63,6 +63,7 @@ export const TRIGGER_TYPES = [
|
||||
"nats",
|
||||
"postgres",
|
||||
"mqtt",
|
||||
"amqp",
|
||||
"sqs",
|
||||
"gcp",
|
||||
"azure",
|
||||
@@ -243,6 +244,8 @@ export async function pushObj(
|
||||
await pushTrigger("postgres", workspace, p, befObj, newObj, permissionedAsContext);
|
||||
} else if (typeEnding === "mqtt_trigger") {
|
||||
await pushTrigger("mqtt", workspace, p, befObj, newObj, permissionedAsContext);
|
||||
} else if (typeEnding === "amqp_trigger") {
|
||||
await pushTrigger("amqp", workspace, p, befObj, newObj, permissionedAsContext);
|
||||
} else if (typeEnding === "sqs_trigger") {
|
||||
await pushTrigger("sqs", workspace, p, befObj, newObj, permissionedAsContext);
|
||||
} else if (typeEnding === "gcp_trigger") {
|
||||
@@ -335,6 +338,7 @@ export function getTypeStrFromPath(
|
||||
| "nats_trigger"
|
||||
| "postgres_trigger"
|
||||
| "mqtt_trigger"
|
||||
| "amqp_trigger"
|
||||
| "sqs_trigger"
|
||||
| "gcp_trigger"
|
||||
| "azure_trigger"
|
||||
@@ -419,6 +423,7 @@ export function getTypeStrFromPath(
|
||||
typeEnding === "nats_trigger" ||
|
||||
typeEnding === "postgres_trigger" ||
|
||||
typeEnding === "mqtt_trigger" ||
|
||||
typeEnding === "amqp_trigger" ||
|
||||
typeEnding === "sqs_trigger" ||
|
||||
typeEnding === "gcp_trigger" ||
|
||||
typeEnding === "azure_trigger" ||
|
||||
|
||||
@@ -285,6 +285,8 @@ export function gitSyncIncludePattern(
|
||||
return `${path}.postgres_trigger.*`;
|
||||
case "mqtttrigger":
|
||||
return `${path}.mqtt_trigger.*`;
|
||||
case "amqptrigger":
|
||||
return `${path}.amqp_trigger.*`;
|
||||
case "sqstrigger":
|
||||
return `${path}.sqs_trigger.*`;
|
||||
case "gcptrigger":
|
||||
|
||||
@@ -274,6 +274,9 @@ describe("gitSyncIncludePattern", () => {
|
||||
expect(gitSyncIncludePattern("gcptrigger", "f/t")).toBe(
|
||||
"f/t.gcp_trigger.*"
|
||||
);
|
||||
expect(gitSyncIncludePattern("amqptrigger", "f/t")).toBe(
|
||||
"f/t.amqp_trigger.*"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user