Files
windmill/backend/windmill-api/src/lib.rs
T
Ruben FiszelandClaude Opus 4.6 c4c9ef5fd7 feat: add optional labels to scripts, flows, apps, schedules, triggers (#8609)
* feat: add optional labels to scripts, flows, apps, raw apps, schedules, and triggers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update sqlx cache, make labels optional in openapi, regenerate system prompts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add minimal labels input UI to script, flow, and schedule editors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce gap between summary and labels input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to script/flow detail pages and summary/path popover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move labels inside SummaryPathDisplay trigger for clickable area, reduce gap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: display labels inline to the right of summary, not below

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase gap between summary and labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to resources/variables, make labels nullable, add home page label filter badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels to workspace export/import, resources, variables + test coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make migration idempotent, regenerate sqlx cache after merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass labels in script create and flow create/update API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels input UI to resource and variable editors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove negative margin from LabelsInput to prevent overlap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add top and left margin to LabelsInput for better spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce left margin on LabelsInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: widen label input to w-32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use inline-flex so LabelsInput doesn't stretch full width

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove flex-wrap so label input stays on same line as badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add label filter presets to resources, variables, and schedules search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use max-w-32 on label input to prevent stretching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pull labels closer to summary with negative top margin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase negative margin to pull labels even closer to summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass labels in schedule create/update API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use COALESCE to preserve existing labels when not provided in schedule/flow update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to CreateResource, EditResource, CreateVariable, EditVariable in OpenAPI spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label badges on resource and variable list pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label badges on schedule and all trigger list pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add folder and label presets to schedules search filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: apply user_folders_only filter on all workspaces including admins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add label presets to resources and variables search filters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: derive folder presets from loaded items, not all workspace folders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add label query parameter to resource and variable list endpoints in OpenAPI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: display label filter badges inline with folder filters on home page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "feat: display label filter badges inline with folder filters on home page"

This reverts commit 6767a50aa6.

* feat: support comma-separated label filters (allowMultiple) in all list endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: append label presets with comma for allowMultiple filters instead of duplicating key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: hide label presets that are already in the comma-separated filter value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace unsafe manual SQL ARRAY construction with parameterized queries, add labels to ScriptWDraft

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: complete down migration, add labels to Resource/Variable OpenAPI schemas, remove type cast, add label length validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels field to Schedule test fixture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels field to Rust client struct constructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: regenerate sqlx cache with --all-features for EE builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx cache and package-lock after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: squash two migrations into one, use IF NOT EXISTS for idempotency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: track label changes in SummaryPathDisplay to enable save button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use JSON string comparison for label dirty tracking in popover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: navigate to script by path after save from popover to load new version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update initialLabels after save so subsequent label changes enable save again

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use onchange callback for label dirty tracking instead of derived comparison

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reload script by path after label save to fetch new version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: propagate script/flow labels to jobs at push time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show script/flow labels on runs page, merge with wm_labels for completed jobs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: change job labels type from JSONB to text[], show labels on job detail page, fix type mismatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to QueuedJob struct, fix get_job queries to return v2_job.labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace +Label text with icon only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add tag icon before labels on job detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move tag icon inside badge on job detail page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use blue badge with tag icon in RunBadges, remove duplicate labels from JobDetailHeader

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: set icon position to left so tag icon renders in badge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: render Tag icon inline in badge children instead of via icon prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: retry icon prop with small badge and position left

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add hover tooltip showing "Label: X" on job label badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: include v2_job.labels in runs page label filter and broad search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx cache and system prompts after merge with main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to EE JobPayload constructions, regenerate sqlx cache with --all-features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: regenerate sqlx cache CE-only (without EE symlinks that cause conflicts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update remaining wm_labels JSONB queries to use text[] merge expression

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify job labels to just read v2_job.labels (wm_labels already merged at completion)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: consistent label badge spacing with gap-0.5 wrapper and px-0.5 on badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels: None to test utils JobPayload construction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add labels to all test fixture JobPayload/NewFlow/EditApp constructions, regenerate sqlx cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix vertical content shift by fixing container and input height to h-5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: npm_check errors - unused imports, combinedItems order, flow.labels type, badge px-1 padding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused FolderService imports, fix label badge alignment in RunBadges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore deleted service imports in variables page, remove empty loadFolders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger CI with updated ee-repo-ref

* chore: update ee-repo-ref to merged EE companion PR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger fresh CI run for updated ee-repo-ref

* fix: match label badge size with other badges in RunBadges using {large} prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove icon from RunBadges label badge to fix vertical alignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: shorten "Job kind" to "Kind" in run badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add small inline tag icon (10px, -mt-px) to label badge without disrupting height

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add "Label: X" hover tooltip to all label badges, show hidden labels on +N hover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add tag icon and "Label: X" tooltip to home page label filter badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show LabelsInput even when path is hidden in ResourceEditor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add labels input to new resource creation drawer (AppConnectInner)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* iterate

* fix: add LabelsInput to all resource creation steps in AppConnectInner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reduce LabelsInput top margin from -mt-3 to -mt-1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase negative margin to -mt-2 for tighter spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: split the difference with -mt-1.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: adjust to -mt-1 for label spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: per-site label spacing via class prop instead of global negative margin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: make label badges clickable to toggle label filter on resources, variables, schedules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use proper array indexOf for label filter toggle, set undefined correctly on removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use delete instead of undefined to properly clear label filter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add /labels/list endpoint and autocomplete dropdown to LabelsInput

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use inline preventDefault for Svelte 5 event handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add "Create new" option in label autocomplete, regenerate sqlx cache with update_sqlx.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add GIN indexes on labels column for all 16 tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove CONCURRENTLY from GIN index creation in migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add comprehensive label coverage for pull, edit, removal across all item types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify job label filters to only use v2_job.labels, remove wm_labels back-compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add integration tests for job label propagation, display, and filtering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review findings — missing labels in fetch_script_for_update, app rename, escape key bug

- Add `labels` to SELECT in `fetch_script_for_update` to prevent lost labels on script clone
- Pass `labels` in app branch of `moveRenameManager.ts` so app renames preserve labels
- Clear `inputValue` before `adding = false` in LabelsInput escape handler to prevent accidental label add via onblur
- Fix `test_job_label_filter` to complete jobs via SQL (label filtering only works on completed jobs)
- Add `test_wm_labels_from_result_merged_with_static_labels` integration test using Bun

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:39:32 +00:00

1156 lines
41 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use crate::db::ApiAuthed;
#[cfg(feature = "embedding")]
use crate::embeddings::load_embeddings_db;
#[cfg(feature = "oauth2")]
use crate::oauth2_oss::SlackVerifier;
#[cfg(feature = "smtp")]
use crate::smtp_server_oss::SmtpServer;
#[cfg(feature = "enterprise")]
use windmill_api_auth::ee_oss::ExternalJwks;
use windmill_store::resources::public_service;
#[cfg(feature = "mcp")]
use crate::mcp::{extract_and_store_workspace_id, setup_mcp_server};
use crate::triggers::start_all_listeners;
use tower_http::catch_panic::CatchPanicLayer;
use crate::tracing_init::MyOnFailure;
use crate::{
s3_log_batching::{s3_proxy_log_middleware, FLUSH_INTERVAL_MS},
tracing_init::{MyMakeSpan, MyOnResponse},
users::OptAuthed,
webhook_util::WebhookShared,
};
#[cfg(feature = "agent_worker_server")]
use windmill_api_agent_workers::AgentCache;
use anyhow::Context;
use argon2::Argon2;
use axum::body::Body;
use axum::extract::DefaultBodyLimit;
use axum::http::HeaderValue;
use axum::response::Response;
use axum::serve::ListenerExt;
use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Json, Router};
use db::DB;
use tokio::task::JoinHandle;
use windmill_common::global_settings::load_value_from_global_settings;
use windmill_common::global_settings::EMAIL_DOMAIN_SETTING;
use windmill_common::worker::HUB_CACHE_DIR;
use std::fs::DirBuilder;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use windmill_common::db::UserDB;
use windmill_common::worker::CLOUD_HOSTED;
#[allow(unused_imports)]
pub(crate) use windmill_common::BASE_URL;
use windmill_common::{utils::GIT_VERSION, INSTANCE_NAME};
use crate::scim_oss::has_scim_token;
use windmill_common::error::AppError;
mod ai;
mod apps;
pub mod args;
mod audit;
pub mod auth;
#[cfg(all(feature = "private", feature = "parquet"))]
pub mod azure_proxy_ee;
mod azure_proxy_oss;
#[cfg(feature = "bedrock")]
mod bedrock;
mod capture;
mod concurrency_groups;
mod db;
mod db_health;
mod google;
mod drafts;
#[cfg(feature = "private")]
pub mod ee;
pub mod ee_oss;
pub mod embeddings;
mod favorite;
pub mod flows;
mod folder_history;
mod folders;
mod granular_acls;
mod group_history;
mod groups;
mod health;
#[cfg(feature = "private")]
pub mod indexer_ee;
mod indexer_oss;
#[cfg(feature = "private")]
mod inkeep_ee;
mod inkeep_oss;
mod integration;
mod internal_db;
mod live_migrations;
#[cfg(all(feature = "private", feature = "parquet"))]
pub mod s3_proxy_ee;
mod s3_proxy_oss;
mod workspace_dependencies;
mod approvals;
#[cfg(all(feature = "enterprise", feature = "private"))]
pub mod apps_ee;
#[cfg(feature = "enterprise")]
mod apps_oss;
#[cfg(all(feature = "enterprise", feature = "private"))]
pub mod git_sync_ee;
#[cfg(feature = "enterprise")]
mod git_sync_oss;
#[cfg(all(feature = "parquet", feature = "private"))]
pub mod job_helpers_ee;
#[cfg(feature = "parquet")]
mod job_helpers_oss;
pub mod job_metrics;
pub mod jobs;
pub mod jobs_export;
#[cfg(all(feature = "oauth2", feature = "private"))]
pub mod oauth2_ee;
#[cfg(feature = "oauth2")]
pub mod oauth2_oss;
#[cfg(feature = "private")]
pub mod oidc_ee;
mod oidc_oss;
mod raw_apps;
mod resources;
#[cfg(feature = "private")]
pub mod saml_ee;
mod saml_oss;
#[cfg(feature = "private")]
pub mod scim_ee;
mod scim_oss;
mod scripts;
mod secret_backend_ext;
mod service_logs;
mod slack_approvals;
#[cfg(all(feature = "smtp", feature = "private"))]
pub mod smtp_server_ee;
#[cfg(feature = "smtp")]
mod smtp_server_oss;
#[cfg(feature = "private")]
pub mod teams_approvals_ee;
mod teams_approvals_oss;
#[cfg(feature = "native_trigger")]
pub mod native_triggers;
mod public_app_layer;
mod public_app_rate_limit;
mod s3_log_batching;
mod static_assets;
#[cfg(all(feature = "stripe", feature = "enterprise", feature = "private"))]
pub mod stripe_ee;
#[cfg(all(feature = "stripe", feature = "enterprise"))]
mod stripe_oss;
#[cfg(feature = "private")]
pub mod teams_cache_ee;
mod teams_cache_oss;
#[cfg(feature = "private")]
pub mod teams_ee;
mod teams_oss;
mod token;
mod tracing_init;
mod trash;
pub mod triggers;
mod users;
#[cfg(feature = "private")]
pub mod users_ee;
mod users_oss;
mod utils;
mod variables;
#[cfg(feature = "private")]
pub mod volumes_ee;
mod volumes_oss;
pub mod webhook_util;
mod workspaces;
#[cfg(feature = "private")]
pub mod workspaces_ee;
mod workspaces_export;
#[cfg(feature = "mcp")]
mod mcp_tools;
#[cfg(feature = "mcp")]
mod mcp;
#[cfg(all(feature = "mcp", feature = "private"))]
mod mcp_oauth_ee;
#[cfg(feature = "mcp")]
mod mcp_oauth_oss;
pub use apps::EditApp;
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
lazy_static::lazy_static! {
pub static ref REQUEST_SIZE_LIMIT: Arc<RwLock<usize>> = Arc::new(RwLock::new(DEFAULT_BODY_LIMIT));
pub static ref SCIM_TOKEN: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref SAML_METADATA: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
// COOKIE_DOMAIN and IS_SECURE are now in windmill_common::utils
}
pub use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
pub use windmill_common::utils::{COOKIE_DOMAIN, IS_SECURE};
#[cfg(feature = "oauth2")]
pub use windmill_oauth::OAUTH_CLIENTS;
#[cfg(feature = "oauth2")]
lazy_static::lazy_static! {
pub static ref SLACK_SIGNING_SECRET: Option<SlackVerifier> = std::env::var("SLACK_SIGNING_SECRET")
.ok()
.map(|x| SlackVerifier::new(x).unwrap());
}
// Compliance with cloud events spec.
pub async fn add_webhook_allowed_origin(
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
if req.method() == http::Method::OPTIONS {
if let Some(webhook_request_origin) = req.headers().get("Webhook-Request-Origin") {
let webhook_request_origin = webhook_request_origin.clone();
let mut response = next.run(req).await;
response
.headers_mut()
.insert("Webhook-Allowed-Origin", webhook_request_origin);
response
.headers_mut()
.insert("Webhook-Allowed-Rate", HeaderValue::from_static("*"));
return response;
}
}
next.run(req).await
}
#[cfg(not(feature = "tantivy"))]
type IndexReader = ();
#[cfg(not(feature = "tantivy"))]
type ServiceLogIndexReader = ();
#[cfg(feature = "tantivy")]
type IndexReader = windmill_indexer::completed_runs_oss::IndexReader;
#[cfg(feature = "tantivy")]
type ServiceLogIndexReader = windmill_indexer::service_logs_oss::ServiceLogIndexReader;
/// Worker name derived from the agent JWT token, used to authenticate volume operations.
/// Defined unconditionally so volume endpoint handlers can reference it regardless of
/// whether agent_worker_server is enabled (the extension is only populated on the agent path).
#[derive(Clone)]
pub struct AgentWorkerName(pub String);
/// Middleware that injects a synthetic `ApiAuthed` and JWT-derived worker name
/// into request extensions.
///
/// Used for volume proxy endpoints under the agent_workers path, where the
/// agent JWT auth layer has already validated the request. The volume handlers
/// need `ApiAuthed` to resolve the workspace S3 client, but the agent JWT
/// format is incompatible with the standard auth extractor.
///
/// The worker name is extracted from the JWT claims rather than trusting
/// self-reported values in request bodies/query params.
#[cfg(feature = "agent_worker_server")]
async fn inject_agent_authed(
request: axum::extract::Request,
next: axum::middleware::Next,
) -> Response {
let mut request = request;
// Extract worker name from agent JWT via AgentCache
// (OSS returns None; EE decodes the JWT and returns the worker name)
{
let extracted = {
let token = request
.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer ").map(|t| t.to_string()));
let cache = request.extensions().get::<Arc<AgentCache>>().cloned();
let db = request.extensions().get::<DB>().cloned();
match (token, cache, db) {
(Some(token), Some(cache), Some(db)) => Some((token, cache, db)),
_ => None,
}
};
if let Some((token, cache, db)) = extracted {
if let Some(worker_name) = cache.extract_worker_name(&token, &db).await {
request
.extensions_mut()
.insert(AgentWorkerName(worker_name));
}
}
}
request
.extensions_mut()
.insert(windmill_api_auth::OptJobAuthed {
authed: ApiAuthed {
email: "agent-worker@windmill.dev".to_string(),
username: "agent-worker".to_string(),
is_admin: true,
is_operator: false,
groups: Vec::new(),
folders: Vec::new(),
scopes: None,
username_override: None,
token_prefix: None,
},
job_id: None,
});
next.run(request).await
}
pub async fn run_server(
db: DB,
job_index_reader: Option<IndexReader>,
log_index_reader: Option<ServiceLogIndexReader>,
listener: tokio::net::TcpListener,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
port_tx: tokio::sync::oneshot::Sender<String>,
server_mode: bool,
mcp_mode: bool,
_base_internal_url: String,
name: Option<String>,
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
for x in [&*HUB_CACHE_DIR] {
DirBuilder::new()
.recursive(true)
.create(x)
.expect("could not create initial server dir");
}
#[cfg(feature = "enterprise")]
let ext_jwks = ExternalJwks::load().await;
let auth_cache = Arc::new(crate::auth::AuthCache::new(
db.clone(),
std::env::var("SUPERADMIN_SECRET").ok(),
#[cfg(feature = "enterprise")]
ext_jwks,
));
let argon2 = Arc::new(Argon2::default());
// Initialize debug signing key for debugger authentication
windmill_api_debug::init_debug_signing_key().await;
let disable_response_logs = std::env::var("DISABLE_RESPONSE_LOGS")
.ok()
.map(|x| x == "true")
.unwrap_or(false);
let middleware_stack = ServiceBuilder::new()
.layer(Extension(db.clone()))
.layer(Extension(user_db.clone()))
.layer(Extension(auth_cache.clone()))
.layer(Extension(job_index_reader))
.layer(Extension(log_index_reader))
// .layer(Extension(index_writer))
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(
killpill_rx.resubscribe(),
db.clone(),
)))
.layer(DefaultBodyLimit::max(
REQUEST_SIZE_LIMIT.read().await.clone(),
));
let request_size_limit = REQUEST_SIZE_LIMIT.read().await.clone();
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST, http::Method::DELETE])
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
let sp_extension = Arc::new(saml_oss::build_sp_extension().await?);
if server_mode {
#[cfg(feature = "embedding")]
load_embeddings_db(&db);
#[cfg(feature = "cloud")]
if *CLOUD_HOSTED {
windmill_queue::init_usage_buffer(db.clone());
}
let mut start_smtp_server = false;
if let Some(smtp_settings) =
load_value_from_global_settings(&db, EMAIL_DOMAIN_SETTING).await?
{
if smtp_settings.as_str().unwrap_or("") != "" {
start_smtp_server = true;
}
}
if !start_smtp_server {
tracing::info!("SMTP server not started because email domain is not set");
} else {
#[cfg(feature = "smtp")]
{
let smtp_server = Arc::new(SmtpServer {
db: db.clone(),
user_db: user_db.clone(),
auth_cache: auth_cache.clone(),
base_internal_url: _base_internal_url.clone(),
});
let addr = listener
.local_addr()
.unwrap_or_else(|_| std::net::SocketAddr::from(([127, 0, 0, 1], 0)));
if let Err(err) = smtp_server.start_listener_thread(addr).await {
tracing::error!("Error starting SMTP server: {err:#}");
}
}
#[cfg(not(feature = "smtp"))]
{
tracing::info!("SMTP server not started because SMTP feature is not enabled");
}
}
}
let job_helpers_service = {
#[cfg(feature = "parquet")]
{
job_helpers_oss::workspaced_service()
}
#[cfg(not(feature = "parquet"))]
{
Router::new()
}
};
// Initialize HTTP trigger refresh loop
#[cfg(feature = "http_trigger")]
{
let http_killpill_rx = killpill_rx.resubscribe();
triggers::http::refresh_routers_loop(&db, http_killpill_rx).await;
}
let triggers_service = triggers::generate_trigger_routers();
if !*CLOUD_HOSTED && server_mode && !mcp_mode {
start_all_listeners(db.clone(), &killpill_rx);
}
if server_mode {
health::start_health_check_loop(db.clone(), killpill_rx.resubscribe());
}
let port = listener.local_addr().map(|x| x.port()).unwrap_or(8000);
let ip = listener
.local_addr()
.map(|x| x.ip().to_string())
.unwrap_or("localhost".to_string());
// Setup MCP server
#[allow(unused_variables)]
let (mcp_router, gateway_mcp_router, mcp_cancellation_token) = {
#[cfg(feature = "mcp")]
if server_mode || mcp_mode {
use mcp::{
add_www_authenticate_header, add_www_authenticate_header_gateway,
extract_workspace_from_token,
};
let (mcp_router, mcp_cancellation_token) =
setup_mcp_server(db.clone(), user_db, _base_internal_url.clone()).await?;
// Workspace-scoped MCP router
let workspaced_mcp_router = mcp_router
.clone()
.route_layer(from_extractor::<ApiAuthed>())
.layer(axum::middleware::from_fn(add_www_authenticate_header))
.layer(axum::middleware::from_fn(extract_and_store_workspace_id));
// Gateway MCP router — resolves workspace from token
let gateway_mcp_router = mcp_router
.route_layer(from_extractor::<ApiAuthed>())
.layer(axum::middleware::from_fn(
add_www_authenticate_header_gateway,
))
.layer(axum::middleware::from_fn(extract_workspace_from_token));
(
workspaced_mcp_router,
gateway_mcp_router,
Some(mcp_cancellation_token),
)
} else {
(Router::new(), Router::new(), None)
}
#[cfg(not(feature = "mcp"))]
(Router::new(), Router::new(), Option::<()>::None)
};
let mcp_list_tools_service = {
#[cfg(feature = "mcp")]
{
mcp::list_tools_service()
}
#[cfg(not(feature = "mcp"))]
{
Router::new()
}
};
#[cfg(feature = "agent_worker_server")]
let (agent_workers_router, agent_workers_bg_processor, agent_workers_job_completed_tx) =
if server_mode {
windmill_api_agent_workers::workspaced_service(db.clone(), _base_internal_url.clone())
} else {
(Router::new(), vec![], None)
};
#[cfg(feature = "agent_worker_server")]
let agent_cache = Arc::new(AgentCache::new());
// build our application with a route
let app = Router::new()
.nest(
"/api",
Router::new()
.nest(
"/w/{workspace_id}",
Router::new()
// Reordered alphabetically
.nest("/acls", granular_acls::workspaced_service())
.nest("/apps", apps::workspaced_service(request_size_limit * 5))
.nest("/assets", windmill_api_assets::workspaced_service())
.nest("/audit", audit::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest(
"/concurrency_groups",
concurrency_groups::workspaced_service(),
)
.nest("/embeddings", embeddings::workspaced_service())
.nest("/drafts", drafts::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest(
"/workspace_dependencies",
workspace_dependencies::workspaced_service(),
)
.nest(
"/flow_conversations",
windmill_api_flow_conversations::workspaced_service(),
)
.nest("/folders", folders::workspaced_service())
.nest("/folders_history", folder_history::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/groups_history", group_history::workspaced_service())
.nest("/inputs", windmill_api_inputs::workspaced_service())
.nest("/internal_db", internal_db::workspaced_service())
.route("/labels/list", get(list_workspace_labels))
.nest("/job_metrics", job_metrics::workspaced_service())
.nest("/job_helpers", job_helpers_service)
.nest("/jobs", jobs::workspaced_service())
.nest("/debug", windmill_api_debug::workspaced_service())
.nest("/native_triggers", {
#[cfg(feature = "native_trigger")]
{
native_triggers::handler::generate_native_trigger_routers().merge(
native_triggers::workspace_integrations::workspaced_service(),
)
}
#[cfg(not(feature = "native_trigger"))]
{
axum::Router::new()
}
})
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_oss::workspaced_service()
}
#[cfg(not(feature = "oauth2"))]
Router::new()
})
.nest("/mcp/oauth/server", {
#[cfg(feature = "mcp")]
{
// Only /approve requires authentication (called by frontend)
mcp::oauth_server::workspaced_authed_service()
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.nest("/ai", ai::workspaced_service())
.nest("/npm_proxy", windmill_api_npm_proxy::workspaced_service())
.nest("/raw_apps", raw_apps::workspaced_service())
.nest("/resources", resources::workspaced_service())
.nest("/schedules", windmill_api_schedule::workspaced_service())
.nest("/scripts", scripts::workspaced_service())
.nest("/trash", trash::workspaced_service())
.nest(
"/users",
users::workspaced_service().layer(Extension(argon2.clone())),
)
.nest("/variables", variables::workspaced_service())
.nest("/volumes", volumes_oss::workspaced_service())
.nest("/workers", windmill_api_workers::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/oidc", oidc_oss::workspaced_service())
.nest("/openapi", {
#[cfg(feature = "http_trigger")]
{
windmill_api_openapi::openapi_service()
}
#[cfg(not(feature = "http_trigger"))]
{
Router::new()
}
})
.merge(triggers_service),
)
.nest("/workspaces", workspaces::global_service())
.nest(
"/users",
users::global_service().layer(Extension(argon2.clone())),
)
.nest("/settings", windmill_api_settings::global_service())
.nest("/workers", windmill_api_workers::global_service())
.nest("/service_logs", service_logs::global_service())
.nest("/configs", windmill_api_configs::global_service())
.nest("/scripts", scripts::global_service())
.nest("/integrations", integration::global_service())
.nest("/groups", groups::global_service())
.nest("/flows", flows::global_service())
.nest("/apps", apps::global_service().layer(cors.clone()))
.nest("/schedules", windmill_api_schedule::global_service())
.nest("/embeddings", embeddings::global_service())
.nest("/ai", ai::global_service())
.nest("/inkeep", inkeep_oss::global_service())
.nest("/indexer", indexer_oss::management_service())
.nest("/mcp/w/{workspace_id}/list_tools", mcp_list_tools_service)
.nest("/db_health", db_health::global_service())
.nest("/health/detailed", health::detailed_service())
.nest(
"/saml",
saml_oss::authed_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest("/mcp/gateway/oauth/server", {
#[cfg(feature = "mcp")]
{
mcp::oauth_server::gateway_authed_service()
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.route_layer(from_extractor::<ApiAuthed>())
.route_layer(from_extractor::<users::Tokened>())
// Workspace-scoped OAuth endpoints that don't require authentication
// (authorize and token are called by MCP client before user is authenticated)
.nest("/w/{workspace_id}/mcp/oauth/server", {
#[cfg(feature = "mcp")]
{
mcp::oauth_server::workspaced_unauthed_service()
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
// Gateway OAuth endpoints (authorize + token) — no auth required
.nest("/mcp/gateway/oauth/server", {
#[cfg(feature = "mcp")]
{
mcp::oauth_server::gateway_unauthed_service().layer(cors.clone())
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.nest("/jobs", jobs::global_root_service())
.nest(
"/srch/w/{workspace_id}/index",
indexer_oss::workspaced_service(),
)
.nest("/srch/index", indexer_oss::global_service())
.nest("/oidc", oidc_oss::global_service())
.nest("/debug", windmill_api_debug::global_service())
.nest(
"/saml",
saml_oss::global_service().layer(Extension(Arc::clone(&sp_extension))),
)
.nest(
"/scim",
scim_oss::global_service()
.route_layer(axum::middleware::from_fn(has_scim_token)),
)
.nest("/tokens", token::global_service())
.nest("/concurrency_groups", concurrency_groups::global_service())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest("/apps_u", {
#[cfg(feature = "enterprise")]
{
apps_oss::global_unauthed_service()
}
#[cfg(not(feature = "enterprise"))]
{
Router::new()
}
})
.nest(
"/w/{workspace_id}/apps_u",
apps::unauthed_service()
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.layer(from_extractor::<OptAuthed>())
// Deprecated, here for backwards compatibility: user should use /mcp/w/{workspace_id}/mcp instead
.nest(
"/mcp/w/{workspace_id}/sse",
mcp_router.clone().layer(cors.clone()),
)
.nest(
"/mcp/w/{workspace_id}/mcp",
mcp_router.clone().layer(cors.clone()),
)
.nest("/mcp/gateway", gateway_mcp_router.layer(cors.clone()))
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
{
if let Some(agent_workers_job_completed_tx) =
agent_workers_job_completed_tx.clone()
{
windmill_api_agent_workers::global_service(
agent_workers_job_completed_tx,
)
.layer(Extension(agent_cache.clone()))
} else {
Router::new()
}
}
#[cfg(not(feature = "agent_worker_server"))]
{
Router::new()
}
})
.nest("/w/{workspace_id}/agent_workers", {
#[cfg(feature = "agent_worker_server")]
{
agent_workers_router
.nest(
"/volumes",
volumes_oss::agent_workspaced_service()
.layer(axum::middleware::from_fn(inject_agent_authed)),
)
.layer(Extension(agent_cache.clone()))
}
#[cfg(not(feature = "agent_worker_server"))]
{
Router::new()
}
})
.nest(
"/w/{workspace_id}/jobs_u",
jobs::workspace_unauthed_service().layer(cors.clone()),
)
.route("/slack", post(slack_approvals::slack_app_callback_handler))
.nest("/teams", {
#[cfg(feature = "enterprise")]
{
teams_oss::teams_service()
}
#[cfg(not(feature = "enterprise"))]
{
Router::new()
}
})
.route(
"/w/{workspace_id}/jobs/slack_approval/{job_id}",
get(slack_approvals::request_slack_approval),
)
.route(
"/w/{workspace_id}/jobs/teams_approval/{job_id}",
get(teams_approvals_oss::request_teams_approval),
)
.nest("/w/{workspace_id}/github_app", {
#[cfg(feature = "enterprise")]
{
git_sync_oss::workspaced_service()
}
#[cfg(not(feature = "enterprise"))]
Router::new()
})
.nest("/github_app", {
#[cfg(feature = "enterprise")]
{
git_sync_oss::global_service()
}
#[cfg(not(feature = "enterprise"))]
Router::new()
})
.nest(
"/w/{workspace_id}/resources_u",
public_service().layer(cors.clone()),
)
.nest(
"/w/{workspace_id}/capture_u",
capture::workspaced_unauthed_service().layer(cors.clone()),
)
.nest("/w/{workspace_id}/s3_proxy", {
s3_proxy_oss::workspaced_unauthed_service()
})
.nest(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),
)
.nest("/oauth", {
#[cfg(feature = "oauth2")]
{
oauth2_oss::global_service().layer(Extension(Arc::clone(&sp_extension)))
}
#[cfg(not(feature = "oauth2"))]
Router::new()
})
.nest("/mcp/oauth", {
#[cfg(feature = "mcp")]
{
mcp_oauth_oss::global_service()
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.nest("/mcp/oauth/server", {
#[cfg(feature = "mcp")]
{
mcp::oauth_server::global_service().layer(cors.clone())
}
#[cfg(not(feature = "mcp"))]
Router::new()
})
.nest("/r", {
#[cfg(feature = "http_trigger")]
{
triggers::http::handler::http_route_trigger_handler()
}
#[cfg(not(feature = "http_trigger"))]
{
Router::new()
}
})
.nest("/gcp/w/{workspace_id}", {
#[cfg(all(
feature = "enterprise",
feature = "gcp_trigger",
feature = "private"
))]
{
triggers::gcp::handler_oss::gcp_push_route_handler()
}
#[cfg(not(all(
feature = "enterprise",
feature = "gcp_trigger",
feature = "private"
)))]
{
Router::new()
}
})
.route("/version", get(git_v))
.nest("/health/status", health::status_service())
.route("/min_keep_alive_version", get(min_keep_alive_version))
.route("/uptodate", get(is_up_to_date))
.route("/ee_license", get(ee_license))
.route("/openapi.yaml", get(openapi))
.route("/openapi.json", get(openapi_json)),
)
// Clients must use workspace-scoped OAuth metadata at:
// /.well-known/oauth-authorization-server/api/w/{workspace_id}/mcp/oauth/server
// This is discovered via /.well-known/oauth-protected-resource?workspace_id=...
.route(
"/.well-known/oauth-authorization-server/api/w/{workspace_id}/mcp/oauth/server",
{
#[cfg(feature = "mcp")]
{
get(mcp::oauth_server::workspaced_oauth_metadata)
}
#[cfg(not(feature = "mcp"))]
{
get(|| async { axum::http::StatusCode::NOT_FOUND })
}
},
)
// RFC 9728 path-based discovery: /.well-known/oauth-protected-resource/api/mcp/w/{workspace_id}/mcp
.route(
"/.well-known/oauth-protected-resource/api/mcp/w/{workspace_id}/mcp",
{
#[cfg(feature = "mcp")]
{
get(mcp::oauth_server::protected_resource_metadata_by_path)
}
#[cfg(not(feature = "mcp"))]
{
get(|| async { axum::http::StatusCode::NOT_FOUND })
}
},
)
// Gateway OAuth well-known endpoints
.route(
"/.well-known/oauth-authorization-server/api/mcp/gateway/oauth/server",
{
#[cfg(feature = "mcp")]
{
get(mcp::oauth_server::gateway_oauth_metadata)
}
#[cfg(not(feature = "mcp"))]
{
get(|| async { axum::http::StatusCode::NOT_FOUND })
}
},
)
.route("/.well-known/oauth-protected-resource/api/mcp/gateway", {
#[cfg(feature = "mcp")]
{
get(mcp::oauth_server::gateway_protected_resource_metadata)
}
#[cfg(not(feature = "mcp"))]
{
get(|| async { axum::http::StatusCode::NOT_FOUND })
}
})
// JWKS endpoint for HashiCorp Vault JWT authentication (must be outside /api prefix)
.route(
"/.well-known/jwks.json",
get(windmill_api_settings::get_jwks),
)
.fallback(static_assets::static_handler)
.layer(middleware_stack);
let app = if disable_response_logs {
app
} else {
tokio::spawn(async {
let mut interval =
tokio::time::interval(std::time::Duration::from_millis(FLUSH_INTERVAL_MS));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
crate::s3_log_batching::flush_s3_batches();
}
});
app.layer(axum::middleware::from_fn(s3_proxy_log_middleware))
.layer(
TraceLayer::new_for_http()
.on_response(MyOnResponse {})
.make_span_with(MyMakeSpan {})
.on_request(())
.on_failure(MyOnFailure {}),
)
};
let app = if let Some(domain) = public_app_layer::PUBLIC_APP_DOMAIN.as_ref() {
tracing::info!("Public app domain filter enabled for domain: {}", domain);
app.layer(axum::middleware::from_fn(
public_app_layer::public_app_domain_filter,
))
} else {
app
};
let app = app.layer(CatchPanicLayer::custom(|err| {
tracing::error!("panic in handler, returning 500: {:?}", err);
Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Internal Server Error"))
.unwrap()
}));
if let Some(name) = name.as_ref() {
tracing::info!("server starting for name={name}");
}
let listener = listener.tap_io(move |tcp_stream| {
let _ = tcp_stream.set_nodelay(!server_mode);
});
let server = axum::serve(listener, app.into_make_service());
tracing::info!(
instance = %*INSTANCE_NAME,
"server started on port={} and addr={} {}",
port,
ip,
name.map(|x| format!("name={x}")).unwrap_or_default()
);
if let Err(e) = port_tx.send(format!("http://localhost:{}", port)) {
tracing::error!("Failed to send port: {e:#}");
return Err(anyhow::anyhow!("Failed to send port, exiting early: {e:#}"));
}
let server = server.with_graceful_shutdown(async move {
killpill_rx.recv().await.ok();
#[cfg(feature = "agent_worker_server")]
if let Some(agent_workers_job_completed_tx) = agent_workers_job_completed_tx {
if let Err(e) = agent_workers_job_completed_tx.kill().await {
tracing::error!("Error killing agent workers: {e:#}");
}
}
tracing::info!("Graceful shutdown of server");
#[cfg(feature = "mcp")]
if let Some(mcp_cancellation_token) = mcp_cancellation_token {
mcp_cancellation_token.cancel();
tracing::info!("MCP server shutdown");
}
});
server.await?;
#[cfg(feature = "agent_worker_server")]
for (i, bg_processor) in agent_workers_bg_processor.into_iter().enumerate() {
tracing::info!("server off. shutting down agent worker bg processor {i}");
bg_processor.await?;
tracing::info!("agent worker bg processor {i} shut down");
}
Ok(())
}
async fn is_up_to_date() -> Result<String, AppError> {
let error_reading_version = || anyhow::anyhow!("Error reading latest released version");
let version = HTTP_CLIENT
.get("https://api.github.com/repos/windmill-labs/windmill/releases/latest")
.timeout(Duration::from_secs(10))
.send()
.await
.context("Impossible to reach api.github")?
.json::<serde_json::Value>()
.await?
.get("tag_name")
.ok_or_else(error_reading_version)?
.as_str()
.ok_or_else(error_reading_version)?
.to_string();
let release = GIT_VERSION
.split('-')
.next()
.ok_or_else(error_reading_version)?
.to_string();
if version == release {
Ok("yes".to_string())
} else {
Ok(format!("Update: {GIT_VERSION} -> {version}"))
}
}
#[cfg(feature = "enterprise")]
async fn git_v() -> String {
format!("EE {GIT_VERSION}")
}
#[cfg(not(feature = "enterprise"))]
async fn git_v() -> String {
format!("CE {GIT_VERSION}")
}
async fn min_keep_alive_version() -> Json<serde_json::Value> {
let worker = windmill_common::min_version::MIN_KEEP_ALIVE_VERSION;
let agent = windmill_common::min_version::AGENT_MIN_KEEP_ALIVE_VERSION;
Json(serde_json::json!({
"worker": format!("{}.{}.{}", worker.0, worker.1, worker.2),
"agent": format!("{}.{}.{}", agent.0, agent.1, agent.2)
}))
}
#[cfg(not(feature = "enterprise"))]
async fn ee_license() -> &'static str {
""
}
async fn list_workspace_labels(
Extension(db): Extension<DB>,
axum::extract::Path(w_id): axum::extract::Path<String>,
) -> windmill_common::error::JsonResult<Vec<String>> {
let labels = sqlx::query_scalar!(
"SELECT DISTINCT unnest(labels) as \"label!\" FROM (
SELECT labels FROM script WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM flow WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM resource WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM variable WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM schedule WHERE workspace_id = $1 AND labels IS NOT NULL
UNION ALL SELECT labels FROM app WHERE workspace_id = $1 AND labels IS NOT NULL
) t ORDER BY 1",
&w_id
)
.fetch_all(&db)
.await?;
Ok(axum::Json(labels))
}
#[cfg(feature = "enterprise")]
async fn ee_license() -> String {
use windmill_common::ee_oss::{LICENSE_KEY_ID, LICENSE_KEY_VALID};
if *LICENSE_KEY_VALID.read().await {
LICENSE_KEY_ID.read().await.clone()
} else {
"".to_string()
}
}
async fn openapi() -> Response {
Response::builder()
.header("content-type", "application/yaml")
.body(Body::from(include_str!("../openapi-deref.yaml")))
.unwrap()
}
async fn openapi_json() -> Response {
Response::builder()
.header("content-type", "application/json")
.body(Body::from(include_str!("../openapi-deref.json")))
.unwrap()
}
pub async fn migrate_db(
db: &DB,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<Option<JoinHandle<()>>> {
db::migrate(db, killpill_rx)
.await
.map_err(|e| anyhow::anyhow!("Error migrating db: {e:#}"))
}
pub async fn wait_for_db_migrations(
db: &DB,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> {
db::wait_for_migrations(db, killpill_rx)
.await
.map_err(|e| anyhow::anyhow!("Error waiting for db migrations: {e:#}"))
}