mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 16:02:24 +00:00
9ff8a85af6
* refactor: extract windmill-api into 4 subcrates (api-auth, store, api-sse, api-jobs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: eliminate refresh_token OnceLock bridge in windmill-store Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: eliminate FromRequestParts OnceLock bridge in windmill-api-auth Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: wire subcrates into workspace and clean up unused re-exports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve cargo check --all-features errors in subcrate wiring Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * sqlx * all * chore: update ee-repo-ref for warning fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract windmill-trigger crate and expand windmill-api-jobs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract windmill-trigger-kafka crate from windmill-api Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract windmill-trigger-postgres crate from windmill-api Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract windmill-trigger-websocket and windmill-trigger-mqtt crates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract windmill-trigger-nats, sqs, gcp, and email crates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract windmill-trigger-http crate from windmill-api Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: move token creation and permission helpers to windmill-api-auth Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract windmill-native-triggers crate from windmill-api Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * sqlx * all * refactor: extract windmill-api-embeddings crate and fix CI warnings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: resolve type mismatch in oauth2_oss and remaining warnings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use correct HTTP_CLIENT config in embeddings crate (30s timeout, cert override) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * all * fix: gate oauth_refresh_ee on oauth2 feature to fix warnings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * all --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
348 lines
11 KiB
Rust
348 lines
11 KiB
Rust
use base64::{engine, prelude::*};
|
|
use itertools::Itertools;
|
|
use rumqttc::{
|
|
v5::{
|
|
mqttbytes::{
|
|
v5::{ConnectProperties, Filter},
|
|
QoS as V5QoS,
|
|
},
|
|
AsyncClient as V5AsyncClient, EventLoop as V5EventLoop, MqttOptions as V5MqttOptions,
|
|
},
|
|
AsyncClient as V3AsyncClient, EventLoop as V3EventLoop, MqttOptions as V3MqttOptions,
|
|
QoS as V3QoS, SubscribeFilter, TlsConfiguration, Transport,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::value::RawValue;
|
|
use sqlx::{types::Json as SqlxJson, FromRow, Type};
|
|
use std::{collections::HashMap, time::Duration};
|
|
use windmill_common::{
|
|
error::{to_anyhow, Error},
|
|
triggers::TriggerKind,
|
|
worker::to_raw_value,
|
|
};
|
|
|
|
use crate::listener::EventLoop;
|
|
use windmill_trigger::trigger_helpers::TriggerJobArgs;
|
|
|
|
pub mod handler;
|
|
pub mod listener;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct MqttTrigger;
|
|
|
|
impl TriggerJobArgs for MqttTrigger {
|
|
type Payload = Vec<u8>;
|
|
const TRIGGER_KIND: TriggerKind = TriggerKind::Mqtt;
|
|
|
|
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(Clone, Debug, Deserialize, Serialize, Type)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum QualityOfService {
|
|
Qos0,
|
|
Qos1,
|
|
Qos2,
|
|
}
|
|
|
|
impl From<QualityOfService> for V3QoS {
|
|
fn from(value: QualityOfService) -> Self {
|
|
match value {
|
|
QualityOfService::Qos0 => V3QoS::AtMostOnce,
|
|
QualityOfService::Qos1 => V3QoS::AtLeastOnce,
|
|
QualityOfService::Qos2 => V3QoS::ExactlyOnce,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<QualityOfService> for V5QoS {
|
|
fn from(value: QualityOfService) -> Self {
|
|
match value {
|
|
QualityOfService::Qos0 => V5QoS::AtMostOnce,
|
|
QualityOfService::Qos1 => V5QoS::AtLeastOnce,
|
|
QualityOfService::Qos2 => V5QoS::ExactlyOnce,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct MqttV3Config {
|
|
clean_session: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct MqttV5Config {
|
|
clean_start: Option<bool>,
|
|
session_expiry_interval: Option<u32>,
|
|
topic_alias_maximum: Option<u16>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, Type)]
|
|
#[sqlx(type_name = "MQTT_CLIENT_VERSION")]
|
|
#[sqlx(rename_all = "lowercase")]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum MqttClientVersion {
|
|
V3,
|
|
V5,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Tls {
|
|
enabled: bool,
|
|
ca_certificate: String,
|
|
pkcs12_client_certificate: Option<String>,
|
|
pkcs12_certificate_password: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Credentials {
|
|
username: Option<String>,
|
|
password: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct MqttResource {
|
|
broker: String,
|
|
port: u16,
|
|
credentials: Option<Credentials>,
|
|
tls: Option<Tls>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, FromRow, Serialize, Deserialize)]
|
|
pub struct SubscribeTopic {
|
|
qos: QualityOfService,
|
|
topic: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
|
pub struct MqttConfig {
|
|
pub mqtt_resource_path: String,
|
|
pub subscribe_topics: Vec<SqlxJson<SubscribeTopic>>,
|
|
pub v3_config: Option<SqlxJson<MqttV3Config>>,
|
|
pub v5_config: Option<SqlxJson<MqttV5Config>>,
|
|
pub client_id: Option<String>,
|
|
pub client_version: Option<MqttClientVersion>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MqttConfigRequest {
|
|
pub mqtt_resource_path: String,
|
|
pub subscribe_topics: Vec<SubscribeTopic>,
|
|
pub v3_config: Option<MqttV3Config>,
|
|
pub v5_config: Option<MqttV5Config>,
|
|
pub client_id: Option<String>,
|
|
pub client_version: Option<MqttClientVersion>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TestMqttConfig {
|
|
pub mqtt_resource_path: String,
|
|
pub client_version: Option<MqttClientVersion>,
|
|
pub v3_config: Option<MqttV3Config>,
|
|
pub v5_config: Option<MqttV5Config>,
|
|
}
|
|
|
|
// Constants
|
|
pub const KEEP_ALIVE: u64 = 60;
|
|
pub const CLIENT_CONNECTION_TIMEOUT: u64 = 60;
|
|
pub const TOPIC_ALIAS_MAXIMUM: u16 = 65535;
|
|
pub const TIMEOUT_DURATION: u64 = 10;
|
|
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(TIMEOUT_DURATION);
|
|
|
|
pub struct V3MqttHandler;
|
|
pub struct V5MqttHandler;
|
|
|
|
pub enum MqttClientResult {
|
|
V3((V3MqttHandler, V3EventLoop)),
|
|
V5((V5MqttHandler, V5EventLoop)),
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum MqttError {
|
|
#[error("{0}")]
|
|
Common(#[from] Error),
|
|
#[error("{0}")]
|
|
V5RumqttClient(#[from] rumqttc::v5::ClientError),
|
|
#[error("{0}")]
|
|
V5ConnectionError(#[from] rumqttc::v5::ConnectionError),
|
|
#[error("{0}")]
|
|
V3RumqttClient(#[from] rumqttc::ClientError),
|
|
#[error("{0}")]
|
|
V3ConnectionError(#[from] rumqttc::ConnectionError),
|
|
#[error("{0}")]
|
|
Base64Decode(#[from] base64::DecodeError),
|
|
}
|
|
|
|
pub struct MqttClientBuilder<'client> {
|
|
mqtt_resource: MqttResource,
|
|
client_id: &'client str,
|
|
subscribe_topics: Vec<SubscribeTopic>,
|
|
v3_config: Option<&'client MqttV3Config>,
|
|
v5_config: Option<&'client MqttV5Config>,
|
|
mqtt_client_version: Option<&'client MqttClientVersion>,
|
|
}
|
|
|
|
impl<'client> MqttClientBuilder<'client> {
|
|
pub fn new(
|
|
mqtt_resource: MqttResource,
|
|
client_id: Option<&'client str>,
|
|
subscribe_topics: Vec<SubscribeTopic>,
|
|
v3_config: Option<&'client MqttV3Config>,
|
|
v5_config: Option<&'client MqttV5Config>,
|
|
mqtt_client_version: Option<&'client MqttClientVersion>,
|
|
) -> Self {
|
|
Self {
|
|
mqtt_resource,
|
|
client_id: client_id.unwrap_or(""),
|
|
subscribe_topics,
|
|
v3_config,
|
|
v5_config,
|
|
mqtt_client_version,
|
|
}
|
|
}
|
|
|
|
pub async fn build_client(&self) -> Result<MqttClientResult, Error> {
|
|
match self.mqtt_client_version {
|
|
Some(MqttClientVersion::V5) | None => self.build_v5_client().await,
|
|
Some(MqttClientVersion::V3) => self.build_v3_client().await,
|
|
}
|
|
}
|
|
|
|
fn get_tls_configuration(&self) -> Result<Option<Transport>, Error> {
|
|
let transport = match self.mqtt_resource.tls {
|
|
Some(ref tls) if tls.enabled => {
|
|
let transport = match tls.ca_certificate.trim().is_empty() {
|
|
true => rumqttc::Transport::Tls(TlsConfiguration::Native),
|
|
false => rumqttc::Transport::Tls(TlsConfiguration::SimpleNative {
|
|
ca: tls.ca_certificate.as_bytes().to_vec(),
|
|
client_auth: {
|
|
match tls.pkcs12_client_certificate.as_ref() {
|
|
Some(client_certificate)
|
|
if !client_certificate.trim().is_empty() =>
|
|
{
|
|
let client_certificate = BASE64_STANDARD
|
|
.decode(client_certificate)
|
|
.map_err(to_anyhow)?;
|
|
let password = tls
|
|
.pkcs12_certificate_password
|
|
.clone()
|
|
.unwrap_or("".to_string());
|
|
Some((client_certificate, password))
|
|
}
|
|
_ => None,
|
|
}
|
|
},
|
|
}),
|
|
};
|
|
|
|
Some(transport)
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
Ok(transport)
|
|
}
|
|
|
|
async fn build_v5_client(&self) -> Result<MqttClientResult, Error> {
|
|
let mut mqtt_options = V5MqttOptions::new(
|
|
self.client_id,
|
|
&self.mqtt_resource.broker,
|
|
self.mqtt_resource.port,
|
|
);
|
|
|
|
if let Some(credentials) = &self.mqtt_resource.credentials {
|
|
let username = credentials.username.as_deref().unwrap_or("");
|
|
let password = credentials.password.as_deref().unwrap_or("");
|
|
mqtt_options.set_credentials(username, password);
|
|
}
|
|
|
|
if let Some(transport) = self.get_tls_configuration()? {
|
|
mqtt_options.set_transport(transport);
|
|
}
|
|
|
|
mqtt_options.set_connection_timeout(CLIENT_CONNECTION_TIMEOUT);
|
|
|
|
mqtt_options.set_keep_alive(Duration::from_secs(KEEP_ALIVE));
|
|
|
|
if let Some(v5_config) = self.v5_config {
|
|
mqtt_options.set_clean_start(v5_config.clean_start.unwrap_or(true));
|
|
mqtt_options.set_connect_properties(ConnectProperties {
|
|
session_expiry_interval: v5_config.session_expiry_interval,
|
|
receive_maximum: None,
|
|
max_packet_size: None,
|
|
topic_alias_max: v5_config.topic_alias_maximum.or(Some(TOPIC_ALIAS_MAXIMUM)),
|
|
request_response_info: None,
|
|
request_problem_info: None,
|
|
user_properties: vec![],
|
|
authentication_method: None,
|
|
authentication_data: None,
|
|
});
|
|
}
|
|
|
|
let (async_client, mut event_loop) =
|
|
V5AsyncClient::new(mqtt_options, self.subscribe_topics.len());
|
|
event_loop.verify_connection().await?;
|
|
|
|
if !self.subscribe_topics.is_empty() {
|
|
let subscribe_filters = self
|
|
.subscribe_topics
|
|
.iter()
|
|
.map(|topic| Filter::new(topic.topic.clone(), topic.qos.clone().into()))
|
|
.collect_vec();
|
|
|
|
async_client
|
|
.subscribe_many(subscribe_filters)
|
|
.await
|
|
.map_err(to_anyhow)?;
|
|
}
|
|
Ok(MqttClientResult::V5((V5MqttHandler, event_loop)))
|
|
}
|
|
|
|
async fn build_v3_client(&self) -> Result<MqttClientResult, Error> {
|
|
let mut mqtt_options = V3MqttOptions::new(
|
|
self.client_id,
|
|
&self.mqtt_resource.broker,
|
|
self.mqtt_resource.port,
|
|
);
|
|
|
|
if let Some(credentials) = &self.mqtt_resource.credentials {
|
|
let username = credentials.username.as_deref().unwrap_or("");
|
|
let password = credentials.password.as_deref().unwrap_or("");
|
|
mqtt_options.set_credentials(username, password);
|
|
}
|
|
|
|
if let Some(transport) = self.get_tls_configuration()? {
|
|
mqtt_options.set_transport(transport);
|
|
}
|
|
mqtt_options.set_keep_alive(Duration::from_secs(KEEP_ALIVE));
|
|
if let Some(v3_config) = self.v3_config {
|
|
mqtt_options.set_clean_session(v3_config.clean_session.unwrap_or(true));
|
|
}
|
|
|
|
let (async_client, mut event_loop) =
|
|
V3AsyncClient::new(mqtt_options, self.subscribe_topics.len());
|
|
event_loop.verify_connection().await?;
|
|
|
|
if !self.subscribe_topics.is_empty() {
|
|
let subscribe_filters = self
|
|
.subscribe_topics
|
|
.iter()
|
|
.map(|topic| SubscribeFilter::new(topic.topic.clone(), topic.qos.clone().into()))
|
|
.collect_vec();
|
|
|
|
async_client
|
|
.subscribe_many(subscribe_filters)
|
|
.await
|
|
.map_err(to_anyhow)?;
|
|
}
|
|
Ok(MqttClientResult::V3((V3MqttHandler, event_loop)))
|
|
}
|
|
}
|