Compare commits
9 Commits
alp/allow_
...
v1.442.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeb712c270 | ||
|
|
0a7268714c | ||
|
|
f5c85d7db9 | ||
|
|
c999788c71 | ||
|
|
0ee7c9ea78 | ||
|
|
2832b887a5 | ||
|
|
bbf7fd695b | ||
|
|
736e241e71 | ||
|
|
c998d2c8c5 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -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"]
|
||||
|
||||
23
backend/.sqlx/query-c73dd6226a154a54debe99f8ae64fd75f8d7ce551c5955aa6bdd8dab2dcbd7ff.json
generated
Normal file
23
backend/.sqlx/query-c73dd6226a154a54debe99f8ae64fd75f8d7ce551c5955aa6bdd8dab2dcbd7ff.json
generated
Normal 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"
|
||||
}
|
||||
91
backend/.sqlx/query-da9114fc6689ebc78422b3572de1ed44050bd28212540db50fe235463c15a900.json
generated
Normal file
91
backend/.sqlx/query-da9114fc6689ebc78422b3572de1ed44050bd28212540db50fe235463c15a900.json
generated
Normal 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
385
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.441.2";
|
||||
export const VERSION = "1.442.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
46
examples/deploy/otel-tracing-jaeger/README.md
Normal file
46
examples/deploy/otel-tracing-jaeger/README.md
Normal 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.
|
||||
201
examples/deploy/otel-tracing-jaeger/docker-compose.yml
Normal file
201
examples/deploy/otel-tracing-jaeger/docker-compose.yml
Normal 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
|
||||
53
examples/deploy/otel-tracing-jaeger/jaeger-config.yaml
Normal file
53
examples/deploy/otel-tracing-jaeger/jaeger-config.yaml
Normal 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"
|
||||
@@ -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
12
flake.lock
generated
@@ -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": {
|
||||
|
||||
76
flake.nix
76
flake.nix
@@ -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";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.441.2",
|
||||
"version": "1.442.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if (itemsType?.type === 'resource' && canEditResourceType) || itemsType?.type !== 'resource'}
|
||||
{#if canEditResourceType}
|
||||
<Label label="Items type">
|
||||
<select
|
||||
bind:value={selected}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.441.2
|
||||
version: 1.442.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.441.2'
|
||||
ModuleVersion = '1.442.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.441.2
|
||||
1.442.0
|
||||
|
||||
Reference in New Issue
Block a user