Compare commits

..

9 Commits

Author SHA1 Message Date
Ruben Fiszel
eeb712c270 chore(main): release 1.442.0 (#4991)
* chore(main): release 1.442.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2025-01-03 18:13:32 +01:00
Alexander Petric
0a7268714c give option to run container as non-root windmill user (#4959)
* main docker file

* fix docker image build test tag

* don't remove tag

* make root user default

* chown tmp folder

* create search and logs folder in order to inherite windmill user permissions

* Dockerfile

* lsp non root

* improving lsp image to get rid of critical vulnerabilities

* formatting

* support /root/.cache mount

* make the cache mount backwards compatible
2025-01-03 16:51:31 +01:00
Lucas Abel
f5c85d7db9 fix(apps): check for auth when executing as publisher (#4979) 2025-01-03 16:47:15 +01:00
Alexander Petric
c999788c71 snowflake resource picker to show both snowflake + snowflake_oauth (#5005) 2025-01-03 16:28:09 +01:00
Lucas Abel
0ee7c9ea78 build: update nix flake (#4998)
- update lock file
- improve npm client package using `pkgs.buildNpmPackage`
- improve windmill package
2025-01-03 16:27:20 +01:00
Alexander Petric
2832b887a5 interactive slack improvements (#5001)
* refactor / rust style

* add detailed description to default message

* default args, dynamic enums

* sqlx prep

* cancel / close behavior
2025-01-03 16:27:03 +01:00
HugoCasa
bbf7fd695b feat: update git sync scripts with url redacted (#5008) 2025-01-03 16:26:48 +01:00
Alexander Petric
736e241e71 adding otel example + small typos in instance settings (#5009) 2025-01-03 16:26:13 +01:00
pyranota
c998d2c8c5 fix(python): Cancel and Start again within 1s caused module not found [v2] (#5007)
* fix(python): Cancel installation and start again within 1s caused module not found

* fix(python): Fix uv install job not exiting on fail
Specifically with nmslib installation was hanging without any output, even tho library build was failed.

It can be monitored with strace or catp.

* Replace ofiles approach with mutex

* Small refactor

* Return space for consistency

* Revert incorrect fix

* Fix module not found

* Remove unused import
2025-01-03 16:09:24 +01:00
44 changed files with 1147 additions and 472 deletions

View File

@@ -1,5 +1,20 @@
# Changelog
## [1.442.0](https://github.com/windmill-labs/windmill/compare/v1.441.2...v1.442.0) (2025-01-03)
### Features
* update git sync scripts with url redacted ([#5008](https://github.com/windmill-labs/windmill/issues/5008)) ([bbf7fd6](https://github.com/windmill-labs/windmill/commit/bbf7fd695b75403714eaa68764137a5fa560c92a))
### Bug Fixes
* **apps:** check for auth when executing as publisher ([#4979](https://github.com/windmill-labs/windmill/issues/4979)) ([f5c85d7](https://github.com/windmill-labs/windmill/commit/f5c85d7db994a269b63940a1445d1c2d659c3bc5))
* **backend:** allow multiple files per field when using multipart ([#5002](https://github.com/windmill-labs/windmill/issues/5002)) ([383ecf8](https://github.com/windmill-labs/windmill/commit/383ecf846754d40ceaee256360a042d881984690))
* handle `'flownode'` job kind where missing ([#4990](https://github.com/windmill-labs/windmill/issues/4990)) ([eeece84](https://github.com/windmill-labs/windmill/commit/eeece84a4b2f09cf50f7a04fac71912953f6e7f1))
* **python:** Cancel and Start again within 1s caused module not found [v2] ([#5007](https://github.com/windmill-labs/windmill/issues/5007)) ([c998d2c](https://github.com/windmill-labs/windmill/commit/c998d2c8c50cc8ec38fb771bd55743fa3e651bcd))
## [1.441.2](https://github.com/windmill-labs/windmill/compare/v1.441.1...v1.441.2) (2024-12-27)

View File

@@ -196,6 +196,21 @@ COPY ./frontend/src/lib/hubPaths.json ${APP}/hubPaths.json
RUN windmill cache ${APP}/hubPaths.json && rm ${APP}/hubPaths.json && chmod -R 777 /tmp/windmill
# Create a non-root user 'windmill' with UID and GID 1000
RUN addgroup --gid 1000 windmill && \
adduser --disabled-password --gecos "" --uid 1000 --gid 1000 windmill
RUN cp -r /root/.cache /home/windmill/.cache
RUN mkdir -p /tmp/windmill/logs && \
mkdir -p /tmp/windmill/search
RUN chown -R windmill:windmill ${APP} && \
chown -R windmill:windmill /tmp/windmill && \
chown -R windmill:windmill /home/windmill/.cache
USER root
EXPOSE 8000
CMD ["windmill"]

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2 LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "c73dd6226a154a54debe99f8ae64fd75f8d7ce551c5955aa6bdd8dab2dcbd7ff"
}

View File

@@ -0,0 +1,91 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n queue.job_kind AS \"job_kind: JobKind\",\n queue.script_hash AS \"script_hash: ScriptHash\",\n queue.raw_flow AS \"raw_flow: sqlx::types::Json<Box<RawValue>>\",\n completed_job.parent_job AS \"parent_job: Uuid\",\n completed_job.created_at AS \"created_at: chrono::NaiveDateTime\",\n completed_job.created_by AS \"created_by!\",\n queue.script_path,\n queue.args AS \"args: sqlx::types::Json<Box<RawValue>>\"\n FROM queue\n JOIN completed_job ON completed_job.parent_job = queue.id\n WHERE completed_job.id = $1 AND completed_job.workspace_id = $2\n LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_kind: JobKind",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript"
]
}
}
}
},
{
"ordinal": 1,
"name": "script_hash: ScriptHash",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "raw_flow: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "parent_job: Uuid",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at: chrono::NaiveDateTime",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "created_by!",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "args: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
true,
true,
true,
false,
false,
true,
true
]
},
"hash": "da9114fc6689ebc78422b3572de1ed44050bd28212540db50fe235463c15a900"
}

385
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.441.2"
version = "1.442.0"
authors.workspace = true
edition.workspace = true
@@ -30,7 +30,7 @@ members = [
]
[workspace.package]
version = "1.441.2"
version = "1.442.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.441.2
version: 1.442.0
title: Windmill API
contact:
@@ -6975,6 +6975,16 @@ paths:
required: true
schema:
type: string
- name: default_args_json
in: query
required: false
schema:
type: string
- name: dynamic_enum_json
in: query
required: false
schema:
type: string
responses:
"200":
description: Interactive slack approval message sent successfully

View File

@@ -35,6 +35,7 @@ use futures::future::{FutureExt, TryFutureExt};
use hyper::StatusCode;
#[cfg(feature = "parquet")]
use itertools::Itertools;
use lazy_static::lazy_static;
use magic_crypt::MagicCryptTrait;
#[cfg(feature = "parquet")]
use object_store::{Attribute, Attributes};
@@ -190,7 +191,7 @@ pub type StaticFields = HashMap<String, Box<RawValue>>;
pub type OneOfFields = HashMap<String, Vec<Box<RawValue>>>;
pub type AllowUserResources = Vec<String>;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionMode {
#[default]
@@ -1374,6 +1375,45 @@ async fn execute_component(
}
};
// Execution is publisher and an user is authenticated: check if the user is authorized to
// execute the app.
if let (ExecutionMode::Publisher, Some(authed)) = (policy.execution_mode, opt_authed.as_ref()) {
lazy_static! {
/// Cache for the permit to execute an app component.
static ref PERMIT_CACHE: cache::Cache<[u8; 32], bool> = cache::Cache::new(1000);
}
// Avoid allocation for the permit key using a sha256 hash of:
// - the user email,
// - the application path,
// - the workspace id.
let permit_key: [u8; 32] = [authed.email.as_bytes(), path.as_bytes(), &w_id.as_bytes()]
.iter()
.fold(Sha256::new(), |hasher, bytes| hasher.chain_update(bytes))
.finalize()
.into();
let permit_fut = PERMIT_CACHE.get_or_insert_async(&permit_key, async {
let mut tx = user_db.clone().begin(authed).await?;
// Permissions are checked by the database; just fetch a row from app using `user_db`:
let row = sqlx::query_scalar!(
"SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2 LIMIT 1",
path,
&w_id,
)
.fetch_optional(&mut *tx)
.await?;
tx.commit().await?;
Result::Ok(row.is_some_and(|x| x.is_some()))
});
if !permit_fut.await? {
return Err(Error::NotAuthorized(format!(
"Missing read permissions on the `{}` app to execute `{}` runnable",
path, payload.component
)));
}
}
let (username, permissioned_as, email) =
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
@@ -1400,7 +1440,7 @@ async fn execute_component(
),
_ => unreachable!(),
};
let tx = windmill_queue::PushIsolationLevel::IsolatedRoot(db.clone());
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
let (uuid, tx) = push(
&db,

View File

@@ -8,7 +8,6 @@ use serde_json::value::{RawValue, Value};
use sqlx::types::Uuid;
use std::{collections::HashMap, str::FromStr};
use windmill_common::error::{self, Error};
use regex::Regex;
use reqwest::Client;
@@ -20,6 +19,8 @@ use crate::jobs::{
};
use windmill_common::{
cache,
error::{self, Error},
jobs::JobKind,
scripts::ScriptHash,
variables::{build_crypt, decrypt_value_with_mc},
@@ -30,22 +31,30 @@ pub struct SlackFormData {
payload: String,
}
#[derive(Deserialize, Debug, Serialize)]
struct Container {
message_ts: String,
channel_id: String,
}
#[derive(Deserialize, Debug)]
struct Payload {
actions: Option<Vec<Action>>,
view: Option<View>,
trigger_id: Option<String>,
#[serde(rename = "type")]
r#type: String,
r#type: PayloadType,
container: Option<Container>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
enum PayloadType {
ViewSubmission,
ViewClosed,
#[serde(other)]
Unknown,
}
#[derive(Deserialize, Debug, Serialize)]
struct Container {
message_ts: String,
channel_id: String,
}
#[derive(Deserialize, Debug)]
struct View {
state: Option<State>,
@@ -53,9 +62,12 @@ struct View {
}
#[derive(Deserialize, Debug)]
struct Action {
value: Option<String>,
action_id: String,
#[serde(tag = "action_id")]
enum Action {
#[serde(rename = "open_modal")]
OpenModal { value: String },
#[serde(other)]
Unknown,
}
#[derive(Deserialize, Debug)]
@@ -97,15 +109,24 @@ struct Schema {
properties: HashMap<String, ResumeFormField>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
enum FieldType {
Boolean,
String,
Number,
Integer,
#[serde(other)]
Unknown,
}
#[derive(Debug, Deserialize, Serialize)]
struct ResumeFormField {
#[serde(rename = "type")]
r#type: String,
r#type: FieldType,
format: Option<String>,
default: Option<serde_json::Value>,
description: Option<String>,
title: Option<String>,
#[serde(rename = "enum")]
r#enum: Option<Vec<String>>,
#[serde(rename = "enumLabels")]
enum_labels: Option<HashMap<String, String>>,
@@ -119,17 +140,27 @@ pub struct QueryMessage {
#[derive(Deserialize)]
pub struct QueryResourcePath {
slack_resource_path: Option<String>,
slack_resource_path: String,
}
#[derive(Deserialize)]
pub struct QueryChannelId {
channel_id: Option<String>,
channel_id: String,
}
#[derive(Deserialize)]
pub struct QueryFlowStepId {
flow_step_id: Option<String>,
flow_step_id: String,
}
#[derive(Deserialize, Debug)]
pub struct QueryDefaultArgsJson {
default_args_json: Option<serde_json::Value>,
}
#[derive(Deserialize, Debug)]
pub struct QueryDynamicEnumJson {
dynamic_enum_json: Option<serde_json::Value>,
}
#[derive(Deserialize, Debug)]
@@ -140,6 +171,8 @@ struct ModalActionValue {
approver: Option<String>,
message: Option<String>,
flow_step_id: Option<String>,
default_args_json: Option<String>,
dynamic_enum_json: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -147,6 +180,7 @@ struct PrivateMetadata {
resume_url: String,
resource_path: String,
container: Container,
hide_cancel: Option<bool>,
}
pub async fn slack_app_callback_handler(
@@ -158,27 +192,20 @@ pub async fn slack_app_callback_handler(
let payload: Payload = serde_json::from_str(&form_data.payload)?;
tracing::debug!("Payload: {:#?}", payload);
match payload.r#type.as_str() {
"view_submission" => {
//print the container
handle_submission(authed, db, &payload, "resume").await?
}
"view_closed" => handle_submission(authed, db, &payload, "cancel").await?,
match payload.r#type {
PayloadType::ViewSubmission => handle_submission(authed, db, &payload, "resume").await?,
PayloadType::ViewClosed => handle_submission(authed, db, &payload, "cancel").await?,
_ => {
if let Some(actions) = payload.actions.as_ref() {
if let Some(actions) = &payload.actions {
if let Some(action) = actions.first() {
match action.action_id.as_str() {
"open_modal" => {
match action {
Action::OpenModal { value } => {
let trigger_id = payload.trigger_id.as_deref().ok_or_else(|| {
Error::BadRequest("No trigger_id found in payload.".to_string())
Error::BadRequest("Missing trigger_id".to_string())
})?;
let value_str = action.value.as_ref().ok_or_else(|| {
Error::BadRequest("No action value found".to_string())
})?;
let parsed_value: ModalActionValue = serde_json::from_str(value_str)
.map_err(|_| {
let parsed_value: ModalActionValue =
serde_json::from_str(value.as_str()).map_err(|_| {
Error::BadRequest("Invalid JSON in action value".to_string())
})?;
@@ -195,6 +222,31 @@ pub async fn slack_app_callback_handler(
Error::BadRequest("No container found.".to_string())
})?;
let default_args_json: Option<serde_json::Value> = parsed_value
.default_args_json
.as_deref()
.map(|s| serde_json::from_str(s))
.transpose()
.map_err(|_| {
Error::BadRequest(
"Invalid JSON in default_args_json".to_string(),
)
})?;
let dynamic_enum_json: Option<serde_json::Value> = parsed_value
.dynamic_enum_json
.as_deref()
.map(|s| serde_json::from_str(s))
.transpose()
.map_err(|_| {
Error::BadRequest(
"Invalid JSON in dynamic_enum_json".to_string(),
)
})?;
tracing::debug!("Default args json: {:#?}", default_args_json);
tracing::debug!("Dynamic enum json: {:#?}", dynamic_enum_json);
open_modal_with_blocks(
&client,
slack_token.as_str(),
@@ -207,17 +259,19 @@ pub async fn slack_app_callback_handler(
message,
flow_step_id,
container,
default_args_json.as_ref(),
dynamic_enum_json.as_ref(),
)
.await
.map_err(|e| {
windmill_common::error::Error::BadRequest(e.to_string())
})?;
.map_err(|e| Error::BadRequest(e.to_string()))?;
}
_ => println!("Unknown action_id: {}", action.action_id),
Action::Unknown => println!("Unknown action_id"),
}
} else {
tracing::debug!("Unknown Slack Action!");
}
} else {
tracing::debug!("Unkown Slack Action!");
tracing::debug!("Unknown Slack Action!");
}
}
}
@@ -234,48 +288,22 @@ pub async fn request_slack_approval(
Query(slack_resource_path): Query<QueryResourcePath>,
Query(channel_id): Query<QueryChannelId>,
Query(flow_step_id): Query<QueryFlowStepId>,
Query(default_args_json): Query<QueryDefaultArgsJson>,
Query(dynamic_enum_json): Query<QueryDynamicEnumJson>,
) -> Result<StatusCode, Error> {
let slack_resource_path = match slack_resource_path.slack_resource_path {
Some(path) => path,
None => {
return Err(windmill_common::error::Error::BadRequest(
"slack_resource_path is required".to_string(),
))
}
};
let channel_id = match channel_id.channel_id {
Some(id) => id,
None => {
return Err(windmill_common::error::Error::BadRequest(
"Slack channel_id is required".to_string(),
))
}
};
let flow_step_id = match flow_step_id.flow_step_id {
Some(id) => id,
None => {
return Err(windmill_common::error::Error::BadRequest(
"Slack flow_step_id is required".to_string(),
))
}
};
let slack_resource_path = slack_resource_path.slack_resource_path;
let channel_id = channel_id.channel_id;
let flow_step_id = flow_step_id.flow_step_id;
let slack_token = get_slack_token(&db, slack_resource_path.as_str(), &w_id).await?;
let client = Client::new();
// Optional fields
let approver_str = approver.approver.as_deref();
let message_str = message.message.as_deref();
tracing::debug!("Approver: {:?}", approver_str);
tracing::debug!("Message: {:?}", message_str);
tracing::debug!("Approver: {:?}", approver.approver);
tracing::debug!("Message: {:?}", message.message);
tracing::debug!("W ID: {:?}", w_id);
tracing::debug!("Slack Resource Path: {:?}", slack_resource_path);
tracing::debug!("Channel ID: {:?}", channel_id);
// Use approver_str and message_str in the function call
send_slack_message(
&client,
slack_token.as_str(),
@@ -283,12 +311,14 @@ pub async fn request_slack_approval(
&w_id,
job_id,
&slack_resource_path,
approver_str,
message_str,
approver.approver.as_deref(),
message.message.as_deref(),
flow_step_id.as_str(),
default_args_json.default_args_json.as_ref(),
dynamic_enum_json.dynamic_enum_json.as_ref(),
)
.await
.map_err(|e| windmill_common::error::Error::BadRequest(e.to_string()))?;
.map_err(|e| Error::BadRequest(e.to_string()))?;
Ok(StatusCode::OK)
}
@@ -325,6 +355,12 @@ async fn handle_submission(
let resume_url = private_metadata.resume_url;
let resource_path = private_metadata.resource_path;
let container: Container = private_metadata.container;
let hide_cancel = private_metadata.hide_cancel;
// If hide_cancel is true, we don't need to extract information from the private_metadata
if hide_cancel.unwrap_or(false) && action == "cancel" {
return Ok(());
}
// Use regex to extract information from private_metadata
let re = Regex::new(r"/api/w/(?P<w_id>[^/]+)/jobs_u/(?P<action>resume|cancel)/(?P<job_id>[^/]+)/(?P<resume_id>[^/]+)/(?P<secret>[a-fA-F0-9]+)(?:\?approver=(?P<approver>[^&]+))?").unwrap();
@@ -393,6 +429,8 @@ async fn transform_schemas(
urls: &ResumeUrls,
order: Option<&Vec<String>>,
required: Option<&Vec<String>>,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
) -> Result<serde_json::Value, Error> {
tracing::debug!("Resume urls: {:#?}", urls);
@@ -408,7 +446,12 @@ async fn transform_schemas(
for key in order.unwrap() {
if let Some(schema) = properties.get(key) {
let is_required = required.unwrap().contains(key);
let input_block = create_input_block(key, schema, is_required);
let default_value = default_args_json.and_then(|json| json.get(key).cloned());
let dynamic_enum_value = dynamic_enum_json.and_then(|json| json.get(key).cloned());
let input_block =
create_input_block(key, schema, is_required, default_value, dynamic_enum_value);
match input_block {
serde_json::Value::Array(arr) => blocks.extend(arr),
_ => blocks.push(input_block),
@@ -420,7 +463,13 @@ async fn transform_schemas(
Ok(serde_json::Value::Array(blocks))
}
fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> serde_json::Value {
fn create_input_block(
key: &str,
schema: &ResumeFormField,
required: bool,
default_value: Option<serde_json::Value>,
dynamic_enum_value: Option<serde_json::Value>,
) -> serde_json::Value {
let placeholder = schema
.description
.as_deref()
@@ -435,16 +484,20 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
};
// Handle boolean type
if schema.r#type == "boolean" {
let initial_value = schema
.default
if let FieldType::Boolean = schema.r#type {
let initial_value = default_value
.as_ref()
.and_then(|default| default.as_bool())
.and_then(|v| v.as_bool())
.or_else(|| {
schema
.default
.as_ref()
.and_then(|default| default.as_bool())
})
.unwrap_or(false);
let mut element = serde_json::json!({
"type": "checkboxes",
"optional": !required,
"options": [{
"text": {
"type": "plain_text",
@@ -471,6 +524,7 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
return serde_json::json!({
"type": "input",
"optional": !required,
"element": element,
"label": {
"type": "plain_text",
@@ -481,80 +535,102 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
}
// Handle date-time format
if schema.r#type == "string" && schema.format.as_deref() == Some("date-time") {
let now = chrono::Local::now();
let current_date = now.format("%Y-%m-%d").to_string();
let current_time = now.format("%H:%M").to_string();
if let FieldType::String = schema.r#type {
if schema.format.as_deref() == Some("date-time") {
tracing::debug!("Date-time type");
let now = chrono::Local::now();
let current_date = now.format("%Y-%m-%d").to_string();
let current_time = now.format("%H:%M").to_string();
let (default_date, default_time) = if let Some(default) = &schema.default {
if let Ok(parsed_date) = chrono::DateTime::parse_from_rfc3339(default.as_str().unwrap())
{
(
parsed_date.format("%Y-%m-%d").to_string(),
parsed_date.format("%H:%M").to_string(),
)
} else {
(current_date.clone(), current_time.clone())
}
} else {
(current_date.clone(), current_time.clone())
};
let (default_date, default_time) = default_value
.as_ref()
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|parsed_date| {
(
parsed_date.format("%Y-%m-%d").to_string(),
parsed_date.format("%H:%M").to_string(),
)
})
.or_else(|| {
schema
.default
.as_ref()
.and_then(|default| {
chrono::DateTime::parse_from_rfc3339(default.as_str().unwrap()).ok()
})
.map(|parsed_date| {
(
parsed_date.format("%Y-%m-%d").to_string(),
parsed_date.format("%H:%M").to_string(),
)
})
})
.unwrap_or((current_date.clone(), current_time.clone()));
return serde_json::json!([
{
"type": "input",
"optional": !required,
"element": {
"type": "datepicker",
"initial_date": &default_date,
"placeholder": {
"type": "plain_text",
"text": "Select a date",
"emoji": true
return serde_json::json!([
{
"type": "input",
"optional": !required,
"element": {
"type": "datepicker",
"initial_date": &default_date,
"placeholder": {
"type": "plain_text",
"text": "Select a date",
"emoji": true
},
"action_id": format!("{}_date", key)
},
"action_id": format!("{}_date", key)
},
"label": {
"type": "plain_text",
"text": title_with_required,
"emoji": true
}
},
{
"type": "input",
"optional": !required,
"element": {
"type": "timepicker",
"initial_time": &default_time,
"placeholder": {
"label": {
"type": "plain_text",
"text": "Select time",
"text": title_with_required,
"emoji": true
},
"action_id": format!("{}_time", key)
}
},
"label": {
"type": "plain_text",
"text": " ",
"emoji": true
{
"type": "input",
"optional": !required,
"element": {
"type": "timepicker",
"initial_time": &default_time,
"placeholder": {
"type": "plain_text",
"text": "Select time",
"emoji": true
},
"action_id": format!("{}_time", key)
},
"label": {
"type": "plain_text",
"text": " ",
"emoji": true
}
}
}
]);
]);
}
}
// Handle enum type
if let Some(enums) = &schema.r#enum {
tracing::debug!("Enum type");
let enums = dynamic_enum_value
.as_ref()
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_else(|| enums.iter().map(|s| serde_json::json!(s)).collect());
let initial_option = schema.default.as_ref().and_then(|default_value| {
enums
.iter()
.find(|enum_value| enum_value == &default_value)
.find(|enum_value| enum_value == &&serde_json::json!(default_value))
.map(|enum_value| {
serde_json::json!({
"text": {
"type": "plain_text",
"text": schema.enum_labels.as_ref()
.and_then(|labels| labels.get(enum_value))
.unwrap_or(enum_value),
.and_then(|labels| labels.get(enum_value.as_str().unwrap()))
.unwrap_or(&enum_value.as_str().unwrap().to_string()),
"emoji": true
},
"value": enum_value
@@ -574,8 +650,8 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
"text": {
"type": "plain_text",
"text": schema.enum_labels.as_ref()
.and_then(|labels| labels.get(enum_value))
.unwrap_or(enum_value),
.and_then(|labels| labels.get(enum_value.as_str().unwrap()))
.unwrap_or(&enum_value.as_str().unwrap().to_string()),
"emoji": true
},
"value": enum_value
@@ -598,15 +674,16 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
"emoji": true
}
})
} else if schema.r#type == "number" || schema.r#type == "integer" {
} else if let FieldType::Number | FieldType::Integer = schema.r#type {
tracing::debug!("Number or integer type");
// Handle number and integer types
let initial_value = schema
.default
let initial_value = default_value
.as_ref()
.and_then(|default| default.as_f64())
.and_then(|v| v.as_f64())
.or_else(|| schema.default.as_ref().and_then(|default| default.as_f64()))
.unwrap_or(0.0);
let action_id_suffix = if schema.r#type == "number" {
let action_id_suffix = if let FieldType::Number = schema.r#type {
"_type_number"
} else {
"_type_integer"
@@ -627,11 +704,12 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
}
})
} else {
tracing::debug!("Other type");
// Handle other types as string
let initial_value = schema
.default
let initial_value = default_value
.as_ref()
.and_then(|default| default.as_str())
.and_then(|v| v.as_str())
.or_else(|| schema.default.as_ref().and_then(|default| default.as_str()))
.unwrap_or("");
serde_json::json!({
@@ -640,7 +718,7 @@ fn create_input_block(key: &str, schema: &ResumeFormField, required: bool) -> se
"element": {
"type": "plain_text_input",
"action_id": key,
"initial_value": initial_value
"initial_value": initial_value.to_string()
},
"label": {
"type": "plain_text",
@@ -787,6 +865,8 @@ async fn send_slack_message(
approver: Option<&str>,
message: Option<&str>,
flow_step_id: &str,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
) -> Result<StatusCode, Box<dyn std::error::Error>> {
let url = "https://slack.com/api/chat.postMessage";
@@ -806,6 +886,14 @@ async fn send_slack_message(
value["message"] = serde_json::json!(message);
}
if let Some(default_args_json) = default_args_json {
value["default_args_json"] = default_args_json.clone();
}
if let Some(dynamic_enum_json) = dynamic_enum_json {
value["dynamic_enum_json"] = dynamic_enum_json.clone();
}
let payload = serde_json::json!({
"channel": channel_id,
"text": "A flow has been suspended. Please approve or reject the flow.",
@@ -867,7 +955,9 @@ async fn get_modal_blocks(
flow_step_id: Option<&str>,
resource_path: &str,
container: Container,
) -> Result<axum::Json<serde_json::Value>, windmill_common::error::Error> {
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
) -> Result<axum::Json<serde_json::Value>, Error> {
let res = get_resume_urls_internal(
axum::Extension(db.clone()),
Path((w_id.to_string(), job_id, resume_id)),
@@ -879,12 +969,16 @@ async fn get_modal_blocks(
tracing::debug!("Job ID: {:?}", job_id);
let (job_kind, script_hash, raw_flow, parent_job_id) = sqlx::query!(
let (job_kind, script_hash, raw_flow, parent_job_id, created_at, created_by, script_path, args) = sqlx::query!(
"SELECT
queue.job_kind AS \"job_kind: JobKind\",
queue.script_hash AS \"script_hash: ScriptHash\",
queue.raw_flow AS \"raw_flow: sqlx::types::Json<Box<RawValue>>\",
completed_job.parent_job AS \"parent_job: Uuid\"
completed_job.parent_job AS \"parent_job: Uuid\",
completed_job.created_at AS \"created_at: chrono::NaiveDateTime\",
completed_job.created_by AS \"created_by!\",
queue.script_path,
queue.args AS \"args: sqlx::types::Json<Box<RawValue>>\"
FROM queue
JOIN completed_job ON completed_job.parent_job = queue.id
WHERE completed_job.id = $1 AND completed_job.workspace_id = $2
@@ -896,15 +990,13 @@ async fn get_modal_blocks(
.await
.map_err(|e| error::Error::BadRequest(e.to_string()))?
.ok_or_else(|| error::Error::BadRequest("This workflow is no longer running and has either already timed out or been cancelled or completed.".to_string()))
.map(|r| (r.job_kind, r.script_hash, r.raw_flow, r.parent_job))?;
.map(|r| (r.job_kind, r.script_hash, r.raw_flow, r.parent_job, r.created_at, r.created_by, r.script_path, r.args))?;
let flow_data = match windmill_common::cache::job::fetch_flow(&db, job_kind, script_hash).await
{
let flow_data = match cache::job::fetch_flow(&db, job_kind, script_hash).await {
Ok(data) => data,
Err(_) => {
if let Some(parent_job_id) = parent_job_id.as_ref() {
windmill_common::cache::job::fetch_preview_flow(&db, parent_job_id, raw_flow)
.await?
cache::job::fetch_preview_flow(&db, parent_job_id, raw_flow).await?
} else {
return Err(error::Error::BadRequest(
"This workflow is no longer running and has either already timed out or been cancelled or completed.".to_string(),
@@ -918,6 +1010,7 @@ async fn get_modal_blocks(
let module = flow_value.modules.iter().find(|m| m.id == flow_step_id);
tracing::debug!("Module: {:#?}", module);
let schema = module.and_then(|module| {
module.suspend.as_ref().map(|suspend| ResumeFormRow {
resume_form: suspend.resume_form.clone(),
@@ -925,14 +1018,36 @@ async fn get_modal_blocks(
})
});
let message_str =
message.unwrap_or("*A workflow has been suspended and is waiting for approval:*\n");
let args_str = args.map_or("None".to_string(), |a| a.get().to_string());
let parent_job_id_str = parent_job_id.map_or("None".to_string(), |id| id.to_string());
let script_path_str = script_path.as_deref().unwrap_or("None");
let created_at_formatted = created_at.format("%Y-%m-%d %H:%M:%S").to_string();
let fallback_message = format!(
"A workflow has been suspended and is waiting for approval:\n\n\
*Created by*: {created_by}\n\
*Created at*: {created_at_formatted}\n\
*Script path*: {script_path_str}\n\
*Args*: {args_str}\n\
*Flow ID*: {parent_job_id_str}\n\n"
);
let mut message_str: String = message.unwrap_or_else(|| fallback_message.as_str()).to_string();
tracing::debug!("Schema: {:#?}", schema);
if let Some(resume_schema) = schema {
let hide_cancel = resume_schema.hide_cancel.unwrap_or(false);
// if hide cancel is false add note to message
if !hide_cancel {
message_str.push_str("\n\n*NOTE*: closing this modal will cancel the workflow.\n\n");
}
// Convert message_str back to &str when needed
let message_str_ref: &str = &message_str;
if let Some(schema_obj) = resume_schema.resume_form {
let inner_schema: ResumeSchema =
serde_json::from_value(schema_obj.clone()).map_err(|e| {
@@ -944,11 +1059,13 @@ async fn get_modal_blocks(
})?;
let blocks = transform_schemas(
message_str,
message_str_ref,
Some(&inner_schema.schema.properties),
&urls,
Some(&inner_schema.schema.order),
Some(&inner_schema.schema.required),
default_args_json,
dynamic_enum_json,
)
.await?;
@@ -963,7 +1080,16 @@ async fn get_modal_blocks(
)));
} else {
tracing::debug!("No suspend form found!");
let blocks = transform_schemas(message_str, None, &urls, None, None).await?;
let blocks = transform_schemas(
message_str_ref,
None,
&urls,
None,
None,
default_args_json,
dynamic_enum_json,
)
.await?;
return Ok(axum::Json(construct_payload(
blocks,
hide_cancel,
@@ -1001,7 +1127,7 @@ fn construct_payload(
"type": "plain_text",
"text": "Resume Workflow"
},
"private_metadata": serde_json::json!({ "resume_url": resume_url, "resource_path": resource_path, "container": container }).to_string(),
"private_metadata": serde_json::json!({ "resume_url": resume_url, "resource_path": resource_path, "container": container, "hide_cancel": hide_cancel }).to_string(),
});
if !hide_cancel {
@@ -1029,6 +1155,8 @@ async fn open_modal_with_blocks(
message: Option<&str>,
flow_step_id: Option<&str>,
container: Container,
default_args_json: Option<&serde_json::Value>,
dynamic_enum_json: Option<&serde_json::Value>,
) -> Result<(), Box<dyn std::error::Error>> {
let resume_id = rand::random::<u32>();
let blocks_json = match get_modal_blocks(
@@ -1042,6 +1170,8 @@ async fn open_modal_with_blocks(
flow_step_id,
resource_path,
container,
default_args_json,
dynamic_enum_json,
)
.await
{

View File

@@ -7,6 +7,7 @@ use std::{
};
use anyhow::anyhow;
use futures::lock::Mutex;
use itertools::Itertools;
use regex::Regex;
use serde_json::value::RawValue;
@@ -35,6 +36,8 @@ use windmill_common::variables::get_secret_value_as_admin;
use windmill_queue::{append_logs, CanceledBy};
lazy_static::lazy_static! {
static ref BUSY_WITH_UV_INSTALL: Mutex<()> = Mutex::new(());
static ref PYTHON_PATH: String =
std::env::var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
@@ -1245,7 +1248,6 @@ async fn spawn_uv_install(
"--target",
venv_p,
"--no-cache",
"-q",
]
};
@@ -1354,6 +1356,7 @@ pub async fn handle_python_reqs(
mut no_uv_install: bool,
is_ansible: bool,
) -> error::Result<Vec<String>> {
let lock = BUSY_WITH_UV_INSTALL.lock().await;
let counter_arc = Arc::new(tokio::sync::Mutex::new(0));
// Append logs with line like this:
// [9/21] + requests==2.32.3 << (S3) | in 57ms
@@ -1484,7 +1487,6 @@ pub async fn handle_python_reqs(
req.replace(' ', "").replace('/', "").replace(':', "")
);
if metadata(&venv_p).await.is_ok() {
// If dir exists skip installation and push path to output
req_paths.push(venv_p);
in_cache.push(req.to_string());
} else {
@@ -1518,6 +1520,12 @@ pub async fn handle_python_reqs(
let pids = Arc::new(tokio::sync::Mutex::new(vec![None; total_to_install]));
let mem_peak_thread_safe = Arc::new(tokio::sync::Mutex::new(0));
{
// when we cancel the job, it has up to 1 second window before actually getting cancelled
// Thus the directory with wheel in windmill's cache cleaned only after that.
// If we manage to start new job during that period windmill might see that wanted wheel is already there (because we have not cleaned it yet)
// and write it to installed wheels, meanwhile previous job will clean that wheel.
// To fix that we create lock, which will pipeline all uv installs on worker
let _lock = lock;
let pids = pids.clone();
let mem_peak_thread_safe = mem_peak_thread_safe.clone();
tokio::spawn(async move {
@@ -1760,10 +1768,12 @@ pub async fn handle_python_reqs(
}
};
let mut stderr = uv_install_proccess
let mut stderr_buf = String::new();
let mut stderr_pipe = uv_install_proccess
.stderr
.take()
.ok_or(anyhow!("Cannot take stderr from uv_install_proccess"))?;
let stderr_future = stderr_pipe.read_to_string(&mut stderr_buf);
if let Some(pid) = pids.lock().await.get_mut(i) {
*pid = uv_install_proccess.id();
@@ -1780,9 +1790,12 @@ pub async fn handle_python_reqs(
uv_install_proccess.kill().await?;
pids.lock().await.get_mut(i).and_then(|e| e.take());
return Err(anyhow::anyhow!("uv pip install was canceled"));
}
// Finished
exitstatus = uv_install_proccess.wait() => match exitstatus {
},
(_, exitstatus) = async {
// See tokio::process::Child::wait_with_output() for more context
// Sometimes uv_install_proccess.wait() is not exiting if stderr is not awaited before it :/
(stderr_future.await, uv_install_proccess.wait().await)
} => match exitstatus {
Ok(status) => if !status.success() {
tracing::warn!(
workspace_id = %w_id,
@@ -1791,24 +1804,18 @@ pub async fn handle_python_reqs(
status.code()
);
let mut buf = String::new();
stderr.read_to_string(&mut buf).await.unwrap_or_else(|_|{
buf = "Cannot read stderr to string".to_owned();
0
});
append_logs(
&job_id,
w_id,
format!(
"\nError while installing {}:\n{buf}",
"\nError while installing {}:\n{stderr_buf}",
&req
),
db,
)
.await;
pids.lock().await.get_mut(i).and_then(|e| e.take());
return Err(anyhow!(buf));
return Err(anyhow!(stderr_buf));
},
Err(e) => {
tracing::error!(

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.441.2";
export const VERSION = "v1.442.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.441.2";
export const VERSION = "1.442.0";
const command = new Command()
.name("wmill")

View File

@@ -143,7 +143,7 @@ services:
expose:
- 3001
volumes:
- lsp_cache:/root/.cache
- lsp_cache:/pyls/.cache
multiplayer:
image: ghcr.io/windmill-labs/windmill-multiplayer:latest

View File

@@ -47,7 +47,7 @@ resource "aws_ecs_task_definition" "windmill_cluster_windmill_lsp_td" {
volume {
name = "lsp_cache"
host_path = "/root/.cache"
host_path = "/pyls/.cache"
}
}

View File

@@ -0,0 +1,46 @@
Tracing with Jaeger
===================
[Jaeger](https://www.jaegertracing.io/) is an open-source distributed tracing system for monitoring and debugging microservices. Originally developed by Uber, it helps track requests across services, analyze latency, identify bottlenecks, and diagnose failures.
Key use cases include debugging production issues, monitoring performance, visualizing service dependencies, and optimizing system reliability. As Jaeger supports the OpenTelemetry protocol, it can be used to collect traces from Windmill.
Follow the guide on [setting up Jaeger](https://windmill.dev/docs/misc/guides/otel#setting-up-jaeger) for more details.
## Setting up Jaeger along with Windmill
Start all services by running:
```bash
docker-compose up -d
```
## Configuring Windmill to use Jaeger
In the Windmill UI available at `http://localhost`, complete the initial setup and go to "Instances Settings" and "OTEL/Prom" tab and fill in the Jaeger endpoint and service name and toggle the Tracing option to send traces to Jaeger.
## Open the Jaeger UI
The Jaeger UI, if hosted with the `docker-compose.yml` file above, will be available at `http://localhost:16686`. When running a script or workflow with Windmill, you will be able to see the traces in the Jaeger UI and investigate them. This can be useful to understand the performance of a workflow and identify bottlenecks in the Windmill server or client.
## Searching for specific traces
To search/filter for a specific trace, for example a workflow, you can use the search function in the Jaeger UI by filtering by tags set by Windmill.
The following tags are useful to filter for specific traces:
- `job_id`: The ID of the job
- `root_job`: The ID of the root job (flow)
- `parent_job`: The ID of the parent job (flow)
- `flow_step_id`: The ID of the step within the workflow
- `script_path`: The path of the script
- `workspace_id`: The name of the workspace
- `worker_id`: The ID of the worker
- `language`: The language of the script
- `tag`: The queue tag of the workflow
## Monitoring metrics with Jaeger
Jaeger can be used to generate time series for metrics of the collected traces. These time series can be used to compare the performance of individual steps within a workflow and their overall performance and relative contribution over time, as well as identify and troubleshoot issues and anomalies.
In the Jaeger UI, you will now be able to see metrics time series for the traces in the "Monitor" tab.

View File

@@ -0,0 +1,201 @@
version: "3.7"
services:
db:
deploy:
# To use an external database, set replicas to 0 and set DATABASE_URL to the external database url in the .env file
replicas: 1
image: postgres:16
shm_size: 1g
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
expose:
- 5432
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: windmill
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
windmill_server:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
restart: unless-stopped
expose:
- 8000
- 2525
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=server
depends_on:
db:
condition: service_healthy
volumes:
- worker_logs:/tmp/windmill/logs
windmill_worker:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 3
resources:
limits:
cpus: "1"
memory: 2048M
# for GB, use syntax '2Gi'
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=default
depends_on:
db:
condition: service_healthy
# to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
# mount the docker socket to allow to run docker containers from within the workers
- /var/run/docker.sock:/var/run/docker.sock
- worker_dependency_cache:/tmp/windmill/cache
- worker_logs:/tmp/windmill/logs
## This worker is specialized for "native" jobs. Native jobs run in-process and thus are much more lightweight than other jobs
windmill_worker_native:
# Use ghcr.io/windmill-labs/windmill-ee:main for the ee
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
resources:
limits:
cpus: "1"
memory: 2048M
# for GB, use syntax '2Gi'
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=native
- NUM_WORKERS=8
- SLEEP_QUEUE=200
depends_on:
db:
condition: service_healthy
volumes:
- worker_logs:/tmp/windmill/logs
# This worker is specialized for reports or scraping jobs. It is assigned the "reports" worker group which has an init script that installs chromium and can be targeted by using the "chromium" worker tag.
# windmill_worker_reports:
# image: ${WM_IMAGE}
# pull_policy: always
# deploy:
# replicas: 1
# resources:
# limits:
# cpus: "1"
# memory: 2048M
# # for GB, use syntax '2Gi'
# restart: unless-stopped
# environment:
# - DATABASE_URL=${DATABASE_URL}
# - MODE=worker
# - WORKER_GROUP=reports
# depends_on:
# db:
# condition: service_healthy
# # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
# volumes:
# # mount the docker socket to allow to run docker containers from within the workers
# - /var/run/docker.sock:/var/run/docker.sock
# - worker_dependency_cache:/tmp/windmill/cache
# - worker_logs:/tmp/windmill/logs
# The indexer powers full-text job and log search, an EE feature.
windmill_indexer:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 0 # set to 1 to enable full-text job and log search
restart: unless-stopped
expose:
- 8001
environment:
- PORT=8001
- DATABASE_URL=${DATABASE_URL}
- MODE=indexer
depends_on:
db:
condition: service_healthy
volumes:
- windmill_index:/tmp/windmill/search
- worker_logs:/tmp/windmill/logs
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest
pull_policy: always
restart: unless-stopped
expose:
- 3001
volumes:
- lsp_cache:/root/.cache
multiplayer:
image: ghcr.io/windmill-labs/windmill-multiplayer:latest
deploy:
replicas: 0 # Set to 1 to enable multiplayer, only available on Enterprise Edition
restart: unless-stopped
expose:
- 3002
caddy:
image: ghcr.io/windmill-labs/caddy-l4:latest
restart: unless-stopped
# Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
# - ./certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caddyfile
ports:
# To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
- 80:80
- 25:25
# - 443:443 # Uncomment to enable HTTPS handling by Caddy
environment:
- BASE_URL=":80"
# - BASE_URL=":443" # uncomment and comment line above to enable HTTPS via custom certificate and key files
# - BASE_URL=mydomain.com # Uncomment and comment line above to enable HTTPS handling by Caddy
# Jaeger OpenTelemetry Example
# https://windmill.dev/docs/misc/guides/otel#setting-up-jaeger
jaeger:
image: jaegertracing/jaeger:latest
ports:
- "16686:16686"
expose:
- 4317
- 8889
volumes:
- ./jaeger-config.yaml:/etc/jaeger/config.yml
command: ["--config", "/etc/jaeger/config.yml"]
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
expose:
- 9090
volumes:
- ./prometheus-config.yaml:/etc/prometheus/prometheus.yml
command:
- "--config.file=/etc/prometheus/prometheus.yml"
volumes:
db_data: null
worker_dependency_cache: null
worker_logs: null
windmill_index: null
lsp_cache: null

View File

@@ -0,0 +1,53 @@
service:
extensions: [jaeger_storage, jaeger_query]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger_storage_exporter, spanmetrics]
metrics/spanmetrics:
receivers: [spanmetrics]
exporters: [prometheus]
telemetry:
resource:
service.name: jaeger
metrics:
level: detailed
address: 0.0.0.0:8888
logs:
level: DEBUG
extensions:
jaeger_query:
storage:
traces: some_storage
metrics: some_metrics_storage
jaeger_storage:
backends:
some_storage:
memory:
max_traces: 100000
metric_backends:
some_metrics_storage:
prometheus:
endpoint: http://prometheus:9090
normalize_calls: true
normalize_duration: true
connectors:
spanmetrics:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
processors:
batch:
exporters:
jaeger_storage_exporter:
trace_storage: some_storage
prometheus:
endpoint: "0.0.0.0:8889"

View File

@@ -0,0 +1,9 @@
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
scrape_configs:
- job_name: aggregated-trace-metrics
static_configs:
- targets: ['jaeger:8889']

12
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1733759999,
"narHash": "sha256-463SNPWmz46iLzJKRzO3Q2b0Aurff3U1n0nYItxq7jU=",
"lastModified": 1735471104,
"narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a73246e2eef4c6ed172979932bc80e1404ba2d56",
"rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4",
"type": "github"
},
"original": {
@@ -61,11 +61,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1733884434,
"narHash": "sha256-8GXR9kC07dyOIshAyfZhG11xfvBRSZzYghnZ2weOKJU=",
"lastModified": 1735612067,
"narHash": "sha256-rsjojgfPUf9tWuMXuuo2KAIoUZ49XGZQJSjFGOO8Cq4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d0483df44ddf0fd1985f564abccbe568e020ddf2",
"rev": "d199142e84bfaae476ffb4e09a70879d7918784d",
"type": "github"
},
"original": {

View File

@@ -19,7 +19,7 @@
];
};
buildInputs = with pkgs; [
openssl openssl.dev libxml2.dev xmlsec.dev libxslt.dev
openssl openssl.dev libxml2.dev xmlsec.dev libxslt.dev libtool
rust nodejs
postgresql
pkg-config cmake
@@ -30,6 +30,21 @@
xmlsec.dev
libxslt.dev
]);
RUSTY_V8_ARCHIVE =
let
version = "130.0.1";
target = pkgs.hostPlatform.rust.rustcTarget;
sha256 = {
x86_64-linux = "sha256-qc25H3Aj2KRhsAZ+2SD1c4RmweVK07oW71opZXRuUoc=";
aarch64-linux = "sha256-qc25H3Aj2KRhsAZ+2SD1c4RmweVK07oW71opZXRuUoc=";
x86_64-darwin = pkgs.lib.fakeHash;
aarch64-darwin = "sha256-d1QTLt8gOUFxACes4oyIYgDF/srLOEk+5p5Oj1ECajQ=";
}.${system};
in pkgs.fetchurl {
name = "librusty_v8-${version}";
url = "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_release_${target}.a.gz";
inherit sha256;
};
in {
devShell = pkgs.mkShell {
buildInputs = buildInputs ++ (with pkgs; [
@@ -45,7 +60,6 @@
'')
(pkgs.writeScriptBin "wm-build" ''
cd ./frontend
echo $(pwd)
npm install
npm run ${if pkgs.stdenv.isDarwin then "generate-backend-client-mac" else "generate-backend-client"}
npm run build $*
@@ -74,7 +88,7 @@
'')
];
inherit PKG_CONFIG_PATH;
inherit PKG_CONFIG_PATH RUSTY_V8_ARCHIVE;
NODE_ENV = "development";
NODE_OPTIONS = "--max-old-space-size=16384";
DATABASE_URL = "postgres://postgres:changeme@127.0.0.1:5432/";
@@ -90,33 +104,41 @@
CARGO_PATH = "${rust}/bin/cargo";
};
packages.default = self.packages.${system}.windmill;
packages.windmill-client = pkgs.stdenv.mkDerivation {
pname = "windmill-client";
packages.windmill-client = pkgs.buildNpmPackage {
name = "windmill-client";
version = (pkgs.lib.strings.trim (builtins.readFile ./version.txt));
src = pkgs.nix-gitignore.gitignoreSource [] ./frontend;
nativeBuildInputs = with pkgs; [ pkg-config ];
buildInputs = with pkgs; [ nodejs pixman cairo pango ];
doCheck = false;
src = ./.;
buildInputs = with pkgs; [ nodejs ];
npmDepsHash = "sha256-NXk9mnf74+/k0i3goqU8Zi/jr5b/bmW+HWRLJCI2CX8=";
npmBuild = "npm run build";
buildPhase = ''
export HOME=$(pwd)
npm config set strict-ssl false
cd frontend
npm install --verbose
postUnpack = ''
mkdir -p ./backend/windmill-api/
cp ${./backend/windmill-api/openapi.yaml} ./backend/windmill-api/openapi.yaml
cp ${./openflow.openapi.yaml} ./openflow.openapi.yaml
'';
preBuild = ''
npm run ${if pkgs.stdenv.isDarwin then "generate-backend-client-mac" else "generate-backend-client"}
NODE_OPTIONS="--max-old-space-size=8192" npm run build
'';
installPhase = ''
mkdir -p $out/build
cp -r build $out/build
cp -r build $out
'';
NODE_OPTIONS = "--max-old-space-size=8192";
};
packages.windmill = pkgs.rustPlatform.buildRustPackage {
pname = "windmill";
version = (pkgs.lib.strings.trim (builtins.readFile ./version.txt));
src = ./backend;
nativeBuildInputs = buildInputs;
nativeBuildInputs = buildInputs ++ [ self.packages.${system}.windmill-client pkgs.perl ];
cargoLock = {
lockFile = ./backend/Cargo.lock;
@@ -128,28 +150,16 @@
};
buildFeatures = [
"embedding" "parquet" "openidconnect" "jemalloc" "deno_core" "license" "http_trigger" "zip" "oauth2" "dind"
"php" "mysql" "mssql" "bigquery" "websocket" "python" "smtp" "csharp" "rust"
"enterprise" "enterprise_saml" "stripe" "embedding" "parquet" "prometheus"
"openidconnect" "cloud" "jemalloc" "tantivy" "deno_core" "license" "http_trigger"
"zip" "oauth2" "kafka" "otel" "dind" "php" "mysql" "mssql" "bigquery" "websocket"
"python" "smtp" "csharp" "static_frontend" "rust"
];
doCheck = false;
inherit PKG_CONFIG_PATH;
inherit PKG_CONFIG_PATH RUSTY_V8_ARCHIVE;
SQLX_OFFLINE = true;
RUSTY_V8_ARCHIVE =
let
version = "130.0.1";
target = pkgs.hostPlatform.rust.rustcTarget;
sha256 = {
x86_64-linux = pkgs.lib.fakeHash;
aarch64-linux = pkgs.lib.fakeHash;
x86_64-darwin = pkgs.lib.fakeHash;
aarch64-darwin = "sha256-d1QTLt8gOUFxACes4oyIYgDF/srLOEk+5p5Oj1ECajQ=";
}.${system};
in pkgs.fetchurl {
name = "librusty_v8-${version}";
url = "https://github.com/denoland/rusty_v8/releases/download/v${version}/librusty_v8_release_${target}.a.gz";
inherit sha256;
};
FRONTEND_BUILD_DIR = "${self.packages.${system}.windmill-client}/build";
};
});
}

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.441.2",
"version": "1.442.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.441.2",
"version": "1.442.0",
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.441.2",
"version": "1.442.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -42,7 +42,7 @@
}
</script>
{#if (itemsType?.type === 'resource' && canEditResourceType) || itemsType?.type !== 'resource'}
{#if canEditResourceType}
<Label label="Items type">
<select
bind:value={selected}

View File

@@ -511,7 +511,7 @@
<div>
<label for="refresh_index_period" class="block text-sm font-medium">
Refresh index period (s) <Tooltip>
The index will query new jobs peridically and write them on the index. This
The index will query new jobs periodically and write them on the index. This
setting sets that period.
</Tooltip></label
>
@@ -548,7 +548,7 @@
<h3>Service Logs Index</h3>
<div>
<label for="commit_log_max_batch_size" class="block text-sm font-medium"
>Commit max batch size Commit max batch size <Tooltip>
>Commit max batch size <Tooltip>
The max amount of documents per commit. In this case 1 document is one log file
representing all logs during 1 minute for a specific host. To optimize indexing
throughput, it is best to keep this as high as possible. However, especially when

View File

@@ -47,12 +47,19 @@
async function loadResources(resourceType: string | undefined) {
loading = true
try {
const nc = (
await ResourceService.listResource({
workspace: $workspaceStore!,
resourceType
})
const resourceTypesToQuery =
resourceType === 'snowflake' ? ['snowflake', 'snowflake_oauth'] : [resourceType]
const resources = await Promise.all(
resourceTypesToQuery.map((rt) =>
ResourceService.listResource({
workspace: $workspaceStore!,
resourceType: rt
})
)
)
const nc = resources
.flat()
.filter((x) => x.resource_type != 'state' && x.resource_type != 'cache')
.map((x) => ({
value: x.path,

View File

@@ -40,6 +40,11 @@
lang: 'snowflake',
argName: 'database'
},
snowflake_oauth: {
code: `select 1`,
lang: 'snowflake',
argName: 'database'
},
ms_sql_server: {
code: `SELECT 1`,
lang: 'mssql',

View File

@@ -137,7 +137,7 @@ ORDER BY
ORDINAL_POSITION;
`
} else if (resourceType === 'snowflake') {
} else if (resourceType === 'snowflake' || resourceType === 'snowflake_oauth') {
code = `
select COLUMN_NAME as field,
DATA_TYPE as DataType,
@@ -380,6 +380,30 @@ return schema
},
argName: 'database'
},
snowflake_oauth: {
code: `select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE from information_schema.columns where table_schema != 'INFORMATION_SCHEMA'`,
lang: 'snowflake',
processingFn: (rows) => {
const schema = {}
for (const row of rows) {
if (!(row.TABLE_SCHEMA in schema)) {
schema[row.TABLE_SCHEMA] = {}
}
if (!(row.TABLE_NAME in schema[row.TABLE_SCHEMA])) {
schema[row.TABLE_SCHEMA][row.TABLE_NAME] = {}
}
schema[row.TABLE_SCHEMA][row.TABLE_NAME][row.COLUMN_NAME] = {
type: row.DATA_TYPE,
required: row.IS_NULLABLE === 'YES'
}
if (row.COLUMN_DEFAULT !== null) {
schema[row.TABLE_SCHEMA][row.TABLE_NAME][row.COLUMN_NAME]['default'] = row.COLUMN_DEFAULT
}
}
return schema
},
argName: 'database'
},
ms_sql_server: {
argName: 'database',
code: `select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT from information_schema.columns where table_schema != 'sys'`,
@@ -560,6 +584,7 @@ export function getLanguageByResourceType(name: string): Preview['language'] {
mysql: 'mysql',
ms_sql_server: 'mssql',
snowflake: 'snowflake',
snowflake_oauth: 'snowflake',
bigquery: 'bigquery'
}
return language[name]

View File

@@ -3713,7 +3713,8 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
mysql: 'MySQL',
ms_sql_server: 'MS SQL Server',
snowflake: 'Snowflake',
bigquery: 'BigQuery'
bigquery: 'BigQuery',
snowflake_oauth: 'Snowflake OAuth'
},
configuration: {
postgresql: {

View File

@@ -73,7 +73,7 @@
<IconSelectInput bind:value={componentInput.value} />
{:else if fieldType === 'tab-select'}
<TabSelectInput bind:componentInput />
{:else if fieldType === 'resource' && subFieldType && ['mysql', 'postgres', 'ms_sql_server', 'snowflake', 'bigquery'].includes(subFieldType)}
{:else if fieldType === 'resource' && subFieldType && ['mysql', 'postgres', 'ms_sql_server', 'snowflake', 'snowflake_oauth', 'bigquery'].includes(subFieldType)}
<ResourcePicker
initialValue={componentInput.value?.split('$res:')?.[1] || ''}
on:change={(e) => {

View File

@@ -38,6 +38,7 @@ export type InputType =
| 'mysql'
| 'ms_sql_server'
| 'snowflake'
| 'snowflake_oauth'
| 'bigquery'
| 'app-path'
@@ -224,6 +225,7 @@ export type AppInput =
| AppInputSpec<'resource', string, 'mysql'>
| AppInputSpec<'resource', string, 'ms_sql_server'>
| AppInputSpec<'resource', string, 'snowflake'>
| AppInputSpec<'resource', string, 'snowflake_oauth'>
| AppInputSpec<'resource', string, 'bigquery'>
| AppInputSpec<'array', object[], 'number-tuple'>
| AppInputSpec<'app-path', string>

View File

@@ -313,7 +313,7 @@ export const settings: Record<string, Setting[]> = {
{
label: 'Prometheus',
description:
'Expose Prometheus metrics for workers and servers on port 8001 at /metrics. <a href="https://www.windmill.dev/docs/advanced/instance_settings#expose-metrics">Learn more</a>',
'Expose Prometheus metrics for workers and servers on port 8001 at /metrics. <a target="_blank" href="https://www.windmill.dev/docs/advanced/instance_settings#expose-metrics">Learn more</a>',
key: 'expose_metrics',
fieldType: 'boolean',
storage: 'setting',

View File

@@ -1,7 +1,9 @@
{
"gitSync_0": "hub/9087/sync-script-to-git-repo-windmill",
"gitSync": "hub/9987/sync-script-to-git-repo-windmill",
"gitSyncTest": "hub/9073/git-repo-test-read-write-windmill",
"gitSync_1": "hub/9987/sync-script-to-git-repo-windmill",
"gitSync": "hub/11498/sync-script-to-git-repo-windmill",
"gitSyncTest_0": "hub/9073/git-repo-test-read-write-windmill",
"gitSyncTest": "hub/11499/git-repo-test-read-write-windmill",
"slackErrorHandler": "hub/9206/workspace-or-schedule-error-handler-slack",
"slackErrorHandler_0": "hub/9079/workspace-or-schedule-error-handler-slack",
"slackRecoveryHandler": "hub/9080/slack/schedule-recovery-handler-slack",
@@ -9,4 +11,4 @@
"slackReport": "hub/9084/slack",
"discordReport": "hub/9085/discord",
"smtpReport": "hub/9086/smtp"
}
}

View File

@@ -1474,12 +1474,12 @@
<Alert type="warning" title="Script version mismatch">
The git sync version for this repository is not latest. Current: <a
target="_blank"
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/5813/versions"
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
>{gitSyncRepository.script_path}</a
>, latest:
<a
target="_blank"
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/5813/versions"
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
>{latestGitSyncHubScript}</a
>
<div class="flex mt-2">

View File

@@ -5,6 +5,8 @@ FROM python-base
COPY --from=node-base /usr/local /usr/local
ENV PATH="/usr/local/bin:${PATH}"
ENV PIPENV_VENV_IN_PROJECT=1
ENV XDG_CACHE_HOME=/pyls/.cache
RUN apt-get update \
&& apt-get install -y shellcheck wget \
@@ -39,12 +41,12 @@ RUN pip3 install tornado python-lsp-jsonrpc ruff-lsp
COPY --from=denoland/deno:2.1.2 --chmod=755 /usr/bin/deno /usr/bin/deno
RUN mkdir -p /pyls/.cache
WORKDIR /pyls
COPY Pipfile .
RUN cat Pipfile
RUN pip install Cython
RUN pipenv install
COPY pyls_launcher.py .
@@ -52,6 +54,9 @@ COPY pyls_launcher.py .
RUN mkdir -p /tmp/monaco && chmod -R 777 /tmp/monaco
RUN cd /tmp/monaco && npm install --save-dev windmill-client
RUN chmod -R a+rX /usr/local && \
chmod -R a+rX /pyls
EXPOSE 3001
CMD ["python3", "pyls_launcher.py"]
CMD ["sh", "-c", "if [ -d /root/.cache ]; then export XDG_CACHE_HOME=/root/.cache && cp -r /pyls/.cache /root/.cache; fi && python3 pyls_launcher.py"]

View File

@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.441.2"
wmill_pg = ">=1.441.2"
wmill = ">=1.442.0"
wmill_pg = ">=1.442.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.441.2
version: 1.442.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.441.2'
ModuleVersion = '1.442.0'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.441.2"
version = "1.442.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -9,6 +9,7 @@ import os
import random
import time
import warnings
import json
from json import JSONDecodeError
from typing import Dict, Any, Union, Literal
@@ -629,6 +630,8 @@ class Windmill:
channel_id: str,
message: str = None,
approver: str = None,
default_args_json: dict = None,
dynamic_enum_json: dict = None,
) -> None:
"""
Request interactive Slack approval
@@ -657,6 +660,10 @@ class Windmill:
params["channel_id"] = channel_id
if os.environ.get("WM_FLOW_STEP_ID"):
params["flow_step_id"] = os.environ.get("WM_FLOW_STEP_ID")
if default_args_json:
params["default_args_json"] = json.dumps(default_args_json)
if dynamic_enum_json:
params["dynamic_enum_json"] = json.dumps(dynamic_enum_json)
self.get(
f"/w/{workspace}/jobs/slack_approval/{os.environ.get('WM_JOB_ID', 'NO_JOB_ID')}",
@@ -1018,12 +1025,16 @@ def request_interactive_slack_approval(
channel_id: str,
message: str = None,
approver: str = None,
default_args_json: dict = None,
dynamic_enum_json: dict = None,
) -> None:
return _client.request_interactive_slack_approval(
slack_resource_path=slack_resource_path,
channel_id=channel_id,
message=message,
approver=approver,
default_args_json=default_args_json,
dynamic_enum_json=dynamic_enum_json,
)
@init_global_client

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.441.2"
version = "1.442.0"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -852,6 +852,8 @@ interface SlackApprovalOptions {
channelId: string;
message?: string;
approver?: string;
defaultArgsJson?: Record<string, any>;
dynamicEnumJson?: Record<string, any>;
}
export async function requestInteractiveSlackApproval({
@@ -859,6 +861,8 @@ export async function requestInteractiveSlackApproval({
channelId,
message,
approver,
defaultArgsJson,
dynamicEnumJson,
}: SlackApprovalOptions): Promise<void> {
const workspace = getWorkspace();
const flowJobId = getEnv("WM_FLOW_JOB_ID");
@@ -881,6 +885,8 @@ export async function requestInteractiveSlackApproval({
slackResourcePath: string;
channelId: string;
flowStepId: string;
defaultArgsJson?: string;
dynamicEnumJson?: string;
} = {
slackResourcePath,
channelId,
@@ -894,6 +900,14 @@ export async function requestInteractiveSlackApproval({
params.approver = approver;
}
if (defaultArgsJson) {
params.defaultArgsJson = JSON.stringify(defaultArgsJson);
}
if (dynamicEnumJson) {
params.dynamicEnumJson = JSON.stringify(dynamicEnumJson);
}
await JobService.getSlackApprovalPayload({
workspace,
...params,

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.441.2",
"version": "1.442.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.441.2",
"version": "1.442.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {

View File

@@ -1 +1 @@
1.441.2
1.442.0