From 01002861e8941cb900c662b159765aeff8682ffe Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 9 Oct 2026 20:11:34 +0200 Subject: [PATCH] feat: pin the default python version of an instance at initialization Co-Authored-By: Claude Opus 5.5 --- Dockerfile | 7 +- backend/src/main.rs | 40 ++++++------ backend/src/monitor.rs | 48 +++++++++----- backend/windmill-api/src/db.rs | 25 ++++++++ .../windmill-common/src/global_settings.rs | 3 + .../windmill-common/src/instance_config.rs | 2 + backend/windmill-common/src/worker.rs | 12 ++++ .../windmill-worker/src/python_versions.rs | 64 +++++++++++++------ backend/windmill-worker/src/worker.rs | 1 + .../src/lib/components/InstanceSetting.svelte | 9 ++- .../src/lib/components/instanceSettings.ts | 8 +-- 11 files changed, 156 insertions(+), 63 deletions(-) diff --git a/Dockerfile b/Dockerfile index fb4583bf1d..c74beffa87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -153,10 +153,9 @@ ARG WITH_HELM=true ARG WITH_GIT=true ARG features="" -# To change latest stable version: -# 1. Change placeholder in instanceSettings.ts -# 2. Change LATEST_STABLE_PY in dockerfile -# 3. Change #[default] annotation for PyVersion in backend +# The version new instances start on: PyVAlias::NEW_INSTANCE in the backend. Instances keep +# the version they were initialized with, so when raising it, keep installing every earlier +# value alongside it. ARG LATEST_STABLE_PY=3.12 ENV UV_PYTHON_INSTALL_DIR=/tmp/windmill/cache/py_runtime ENV UV_PYTHON_PREFERENCE=only-managed diff --git a/backend/src/main.rs b/backend/src/main.rs index 66ddce60e2..d2c7ab9fc2 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -9,7 +9,8 @@ use anyhow::Context; use monitor::{ flush_pending_log_files_to_object_store, load_base_url, load_otel, reload_critical_alerts_on_db_oversize, reload_delete_logs_periodically_setting, - reload_indexer_config, reload_instance_python_version_setting, reload_maven_repos_setting, + reload_indexer_config, reload_instance_python_baseline_setting, + reload_instance_python_version_setting, reload_maven_repos_setting, reload_maven_settings_xml_setting, reload_no_default_maven_setting, reload_nuget_config_setting, reload_powershell_repo_pat_setting, reload_powershell_repo_url_setting, reload_ruby_repos_setting, @@ -49,23 +50,23 @@ use windmill_common::{ EXTRA_PIP_INDEX_URL_SETTING, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS_SETTING, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INDEXER_SETTING, - INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING, - JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, - KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MAVEN_SETTINGS_XML_SETTING, - MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, - NO_DEFAULT_MAVEN_SETTING, NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, - NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, - OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, - POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, - REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, - RESTART_COORDINATION_SETTING, RETENTION_PERIOD_SECS_OVERRIDES_SETTING, - RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING, SAML_METADATA_SETTING, - SANDBOX_IMAGE_CACHE_MAX_MB_SETTING, SANDBOX_IMAGE_DEFAULT_REGISTRY_SETTING, - SANDBOX_IMAGE_MAX_SIZE_MB_SETTING, SANDBOX_IMAGE_PULL_POLICY_SETTING, - SANDBOX_REGISTRY_AUTH_SETTING, SCIM_TOKEN_SETTING, SERVICE_LOG_RETENTION_SECS_SETTING, - SMTP_SETTING, STORE_AUDIT_LOGS_S3_SETTING, SYNC_HUB_RESOURCE_TYPES_DAILY_SETTING, - TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING, UV_EXCLUDE_NEWER_SETTING, - UV_INDEX_STRATEGY_SETTING, UV_PYTHON_INSTALL_MIRROR_SETTING, + INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_BASELINE_SETTING, + INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, + JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, + MAVEN_SETTINGS_XML_SETTING, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, + MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING, + NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, + NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, + OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, + POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING, + REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RESTART_COORDINATION_SETTING, + RETENTION_PERIOD_SECS_OVERRIDES_SETTING, RETENTION_PERIOD_SECS_SETTING, RUBY_REPOS_SETTING, + SAML_METADATA_SETTING, SANDBOX_IMAGE_CACHE_MAX_MB_SETTING, + SANDBOX_IMAGE_DEFAULT_REGISTRY_SETTING, SANDBOX_IMAGE_MAX_SIZE_MB_SETTING, + SANDBOX_IMAGE_PULL_POLICY_SETTING, SANDBOX_REGISTRY_AUTH_SETTING, SCIM_TOKEN_SETTING, + SERVICE_LOG_RETENTION_SECS_SETTING, SMTP_SETTING, STORE_AUDIT_LOGS_S3_SETTING, + SYNC_HUB_RESOURCE_TYPES_DAILY_SETTING, TEAMS_SETTING, TIMEOUT_WAIT_RESULT_SETTING, + UV_EXCLUDE_NEWER_SETTING, UV_INDEX_STRATEGY_SETTING, UV_PYTHON_INSTALL_MIRROR_SETTING, WORKSPACE_FAIRNESS_DURATION_SECS_SETTING, WORKSPACE_FAIRNESS_ENABLED_SETTING, WORKSPACE_FAIRNESS_MAX_PERCENT_SETTING, WORKSPACE_FAIRNESS_MIN_TOTAL_SETTING, WORKSPACE_MAX_QUEUED_JOBS_SETTING, WORKSPACE_REGISTRIES_SETTING, @@ -1920,6 +1921,9 @@ async fn process_notify_event( INSTANCE_PYTHON_VERSION_SETTING => { reload_instance_python_version_setting(conn).await } + INSTANCE_PYTHON_BASELINE_SETTING => { + reload_instance_python_baseline_setting(conn).await + } NPM_CONFIG_REGISTRY_SETTING => reload_npm_config_registry_setting(conn).await, BUNFIG_INSTALL_SCOPES_SETTING => reload_bunfig_install_scopes_setting(conn).await, NUGET_CONFIG_SETTING => reload_nuget_config_setting(conn).await, diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 8de85baa24..a68a971e3c 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -63,14 +63,15 @@ use windmill_common::{ DEFAULT_TAGS_WORKSPACES_SETTING, DEPENDENCY_JOB_TAG_SETTING, DISABLE_PASSWORD_LOGIN, DISABLE_PASSWORD_LOGIN_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, - HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, - JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, - KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MCP_DISABLE_TOKEN_QUERY_PARAM, - MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING, - NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, - NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, - OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, - POWERSHELL_REPO_URL_SETTING, PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING, + HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_BASELINE_SETTING, + INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, + JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, + MCP_DISABLE_TOKEN_QUERY_PARAM, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, + MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, + NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING, + OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING, + PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, + PREVIEW_TAGS_OVERRIDE_SETTING, REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SANDBOX_IMAGE_CACHE_MAX_MB_SETTING, SANDBOX_IMAGE_DEFAULT_REGISTRY_SETTING, SANDBOX_IMAGE_MAX_SIZE_MB_SETTING, @@ -130,14 +131,14 @@ use windmill_store::resources::MAX_RESOURCE_VERSIONS; use windmill_worker::{ result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel, OtelTracingProxySettings, SameWorkerSender, StepFailureKind, WorkspaceRegistryMap, - BUNFIG_INSTALL_SCOPES, BUN_INSTALL_MIN_RELEASE_AGE, CARGO_REGISTRIES, INSTANCE_PYTHON_VERSION, - JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR, MAVEN_REPOS, - MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE, - NSJAIL_TMPFS_SIZE_MB, NSJAIL_TMP_BACKING, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, - PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, - SANDBOX_IMAGE_CACHE_MAX_MB, SANDBOX_IMAGE_DEFAULT_REGISTRY, SANDBOX_IMAGE_MAX_SIZE_MB, - SANDBOX_IMAGE_PULL_POLICY, SANDBOX_REGISTRY_AUTH, UNSHARE_PATH, UV_EXCLUDE_NEWER, - UV_INDEX_STRATEGY, UV_PYTHON_INSTALL_MIRROR, WORKSPACE_REGISTRIES, + BUNFIG_INSTALL_SCOPES, BUN_INSTALL_MIN_RELEASE_AGE, CARGO_REGISTRIES, INSTANCE_PYTHON_BASELINE, + INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR, + MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY, + NSJAIL_AVAILABLE, NSJAIL_TMPFS_SIZE_MB, NSJAIL_TMP_BACKING, NUGET_CONFIG, + OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT, + POWERSHELL_REPO_URL, SANDBOX_IMAGE_CACHE_MAX_MB, SANDBOX_IMAGE_DEFAULT_REGISTRY, + SANDBOX_IMAGE_MAX_SIZE_MB, SANDBOX_IMAGE_PULL_POLICY, SANDBOX_REGISTRY_AUTH, UNSHARE_PATH, + UV_EXCLUDE_NEWER, UV_INDEX_STRATEGY, UV_PYTHON_INSTALL_MIRROR, WORKSPACE_REGISTRIES, }; #[cfg(feature = "parquet")] @@ -713,6 +714,11 @@ pub async fn initial_load( "INSTANCE_PYTHON_VERSION", INSTANCE_PYTHON_VERSION.clone(), ); + pass.option_setting( + INSTANCE_PYTHON_BASELINE_SETTING, + "INSTANCE_PYTHON_BASELINE", + INSTANCE_PYTHON_BASELINE.clone(), + ); pass.option_setting(NUGET_CONFIG_SETTING, "NUGET_CONFIG", NUGET_CONFIG.clone()); pass.option_setting( POWERSHELL_REPO_URL_SETTING, @@ -2896,6 +2902,16 @@ pub async fn reload_instance_python_version_setting(conn: &Connection) { .await; } +pub async fn reload_instance_python_baseline_setting(conn: &Connection) { + reload_option_setting_with_tracing( + conn, + INSTANCE_PYTHON_BASELINE_SETTING, + "INSTANCE_PYTHON_BASELINE", + INSTANCE_PYTHON_BASELINE.clone(), + ) + .await; +} + pub async fn reload_npm_config_registry_setting(conn: &Connection) { reload_option_setting_with_tracing( conn, diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 9619e96ebc..f7f86260f2 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -17,7 +17,9 @@ use tokio::task::JoinHandle; pub use windmill_common::db::DB; use windmill_common::{ error::Error, + global_settings::INSTANCE_PYTHON_BASELINE_SETTING, utils::{generate_lock_id, GIT_VERSION}, + worker::PyVAlias, }; #[allow(unused_imports)] @@ -372,12 +374,35 @@ pub async fn migrate( } crate::live_migrations::custom_migrations(&mut custom_migrator).await?; + if let Err(e) = seed_instance_python_baseline(db).await { + tracing::error!("Could not record the instance python baseline: {e:#}"); + } Ok(Some(crate::live_migrations::spawn_background_migrations( db.clone(), killpill_rx, ))) } +/// Records which Python version this instance runs on while `instance_python_version` is +/// unset, once. An instance that predates the record keeps the frozen default; only one whose +/// first migration ran within the last day counts as new, since a database has no other +/// mark of its age and a restore or an upgrade keeps the original migration dates. +async fn seed_instance_python_baseline(db: &DB) -> Result<(), Error> { + sqlx::query( + "INSERT INTO global_settings (name, value) + SELECT $1, to_jsonb(CASE + WHEN (SELECT min(installed_on) FROM _sqlx_migrations) > now() - interval '1 day' + THEN $2::text ELSE $3::text END) + ON CONFLICT (name) DO NOTHING", + ) + .bind(INSTANCE_PYTHON_BASELINE_SETTING) + .bind(PyVAlias::NEW_INSTANCE.version_string()) + .bind(PyVAlias::default().version_string()) + .execute(db) + .await?; + Ok(()) +} + pub async fn wait_for_migrations( db: &DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>, diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 2bc303f5c2..3a1a19ef9a 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -52,6 +52,9 @@ pub const UV_EXCLUDE_NEWER_SETTING: &str = "uv_exclude_newer"; pub const UV_PYTHON_INSTALL_MIRROR_SETTING: &str = "uv_python_install_mirror"; pub const BUN_INSTALL_MIN_RELEASE_AGE_SETTING: &str = "bun_install_min_release_age"; pub const INSTANCE_PYTHON_VERSION_SETTING: &str = "instance_python_version"; +/// The Python version an instance uses while `instance_python_version` is unset. Written once +/// by the server, so raising the version new instances start on never moves an existing one. +pub const INSTANCE_PYTHON_BASELINE_SETTING: &str = "instance_python_baseline"; pub const RUFF_CONFIG_SETTING: &str = "ruff_config"; pub const SCIM_TOKEN_SETTING: &str = "scim_token"; pub const SAML_METADATA_SETTING: &str = "saml_metadata"; diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index 3d50558c7f..49f5cace41 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -1026,6 +1026,7 @@ pub const PROTECTED_SETTINGS: &[&str] = &[ "rsa_keys", "jwt_secret", "min_keep_alive_version", + "instance_python_baseline", ]; /// Secrets the server signs with or generated for itself. `jwt_secret` signs API and job @@ -1065,6 +1066,7 @@ pub fn is_withheld_server_secret(name: &str) -> bool { pub const HIDDEN_SETTINGS: &[&str] = &[ "uid", "min_keep_alive_version", + "instance_python_baseline", "automate_username_creation", "_restart_coordination", // Legacy ghost: worker configs live in the `config` table with a diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 9bc788d9f3..04172b2f64 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -2889,6 +2889,8 @@ pub fn lockfile_line_has_continuation(line: &str) -> bool { pub enum PyVAlias { Py310 = 10, Py311, + // Frozen: what an instance with no recorded baseline and a lock with no `# py:` line run + // on. Raise `NEW_INSTANCE` instead, which existing instances never pick up. #[default] Py312, Py313, @@ -2907,10 +2909,20 @@ impl Into for PyVAlias { } impl PyVAlias { + /// The version a newly initialized instance records as its baseline. Raising it also + /// means the images must keep preinstalling every earlier value, or instances that + /// recorded one download their runtime on first use. + pub const NEW_INSTANCE: PyVAlias = PyVAlias::Py312; + pub fn all>() -> Vec { use PyVAlias::*; vec![Py310.into(), Py311.into(), Py312.into(), Py313.into()] } + /// `MAJOR.MINOR`, the form the instance settings store. + pub fn version_string(self) -> String { + format!("{}.{}", self.major(), self as u32) + } + // Get MAJOR part of alias. (semver: MAJOR.MINOR.PATCH) fn major(&self) -> u32 { use PyVAlias::*; diff --git a/backend/windmill-worker/src/python_versions.rs b/backend/windmill-worker/src/python_versions.rs index 11674efcd6..b45f923d78 100644 --- a/backend/windmill-worker/src/python_versions.rs +++ b/backend/windmill-worker/src/python_versions.rs @@ -26,8 +26,8 @@ use crate::{ common::{start_child_process, OccupancyMetrics}, handle_child::handle_child, python_executor::PYTHON_PATH, - HOME_ENV, INDEX_CERT, INSTANCE_PYTHON_VERSION, NATIVE_CERT, PATH_ENV, PROXY_ENVS, - PY_INSTALL_DIR, UV_CACHE_DIR, UV_PYTHON_INSTALL_MIRROR, WIN_ENVS, + HOME_ENV, INDEX_CERT, INSTANCE_PYTHON_BASELINE, INSTANCE_PYTHON_VERSION, NATIVE_CERT, PATH_ENV, + PROXY_ENVS, PY_INSTALL_DIR, UV_CACHE_DIR, UV_PYTHON_INSTALL_MIRROR, WIN_ENVS, }; impl From for PyVAlias { @@ -49,10 +49,6 @@ impl From for PyVAlias { } } -// To change latest stable version: -// 1. Change placeholder in instanceSettings.ts -// 2. Change LATEST_STABLE_PY in dockerfile -// 3. Change #[default] annotation for PyVersion in backend #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct PyV(pub pep440_rs::Version); @@ -87,6 +83,31 @@ impl DerefMut for PyV { } } +/// The version scripts with no pin of their own resolve around: the admin's choice, else the +/// baseline the instance recorded, else the frozen default. A value that does not parse is +/// skipped and reported rather than failing the job. +fn instance_python_version(setting: Option<&str>, baseline: Option<&str>) -> (PyV, Option) { + let mut err = None; + for (name, value) in [ + ( + "instance_python_version", + setting.filter(|v| *v != "default"), + ), + ("instance_python_baseline", baseline), + ] { + let Some(value) = value else { continue }; + match pep440_rs::Version::from_str(value) { + Ok(v) => return (v.into(), err), + Err(_) => { + err.get_or_insert_with(|| { + format!("\nCannot parse {name} ({value:?}), ignoring it") + }); + } + } + } + (PyV::default(), err) +} + impl PyV { pub async fn resolve( version_specifiers: Vec, @@ -259,17 +280,10 @@ impl PyV { w_id: &str, conn: Option, ) -> Self { - let mut err = None; - let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() { - Some(v) if &v == "default" => PyVAlias::default().into(), - Some(v) => pep440_rs::Version::from_str(&v).unwrap_or_else(|_| { - let v = PyVAlias::default().into(); - err = Some(format!("\nCannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})", *INSTANCE_PYTHON_VERSION)); - v - }), - // Use latest stable - None => PyVAlias::default().into(), - }; + let (pyv, err) = instance_python_version( + INSTANCE_PYTHON_VERSION.read().await.as_deref(), + INSTANCE_PYTHON_BASELINE.read().await.as_deref(), + ); if let Some(msg) = err { if let Some(conn) = conn { @@ -277,7 +291,7 @@ impl PyV { } tracing::error!(msg); } - pyv.into() + pyv } pub async fn list_available_python_versions() -> Vec { @@ -785,6 +799,20 @@ mod tests { pep440_rs::Version::from_str(value).unwrap().into() } + #[test] + fn instance_version_prefers_the_setting_then_the_baseline() { + let version = |setting, baseline| instance_python_version(setting, baseline).0; + assert_eq!(version(Some("3.11"), Some("3.13")), pyv("3.11")); + // "default" is what the settings UI stores for "no choice". + assert_eq!(version(Some("default"), Some("3.13")), pyv("3.13")); + assert_eq!(version(None, Some("3.13")), pyv("3.13")); + assert_eq!(version(None, None), PyV::default()); + + let (v, err) = instance_python_version(Some("nope"), Some("3.13")); + assert_eq!(v, pyv("3.13")); + assert!(err.is_some()); + } + async fn assert_resolution( instance_version: &str, select_highest: bool, diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 5ae626f27e..d83cec3fc5 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -665,6 +665,7 @@ lazy_static::lazy_static! { pub static ref UV_EXCLUDE_NEWER: Arc>> = Arc::new(RwLock::new(None)); pub static ref BUN_INSTALL_MIN_RELEASE_AGE: Arc>> = Arc::new(RwLock::new(None)); pub static ref INSTANCE_PYTHON_VERSION: Arc>> = Arc::new(RwLock::new(None)); + pub static ref INSTANCE_PYTHON_BASELINE: Arc>> = Arc::new(RwLock::new(None)); pub static ref JOB_DEFAULT_TIMEOUT: Arc>> = Arc::new(RwLock::new(None)); diff --git a/frontend/src/lib/components/InstanceSetting.svelte b/frontend/src/lib/components/InstanceSetting.svelte index d6740c6c9b..bf3a4f883d 100644 --- a/frontend/src/lib/components/InstanceSetting.svelte +++ b/frontend/src/lib/components/InstanceSetting.svelte @@ -199,10 +199,15 @@ let pythonAvailableVersions: ListAvailablePythonVersionsResponse = $state([]) + let pythonBaseline: string | undefined = $state() + let isPyFetching = $state(false) async function fetch_available_python_versions() { if (isPyFetching) return isPyFetching = true + SettingService.getGlobal({ key: 'instance_python_baseline' }) + .then((v) => (pythonBaseline = typeof v === 'string' ? v : undefined)) + .catch((error) => console.error('Error fetching python baseline:', error)) try { pythonAvailableVersions = await ConfigService.listAvailablePythonVersions() } catch (error) { @@ -269,7 +274,9 @@ {#each setting.select_items ?? [] as item} diff --git a/frontend/src/lib/components/instanceSettings.ts b/frontend/src/lib/components/instanceSettings.ts index 867570e1a1..deeb67c78c 100644 --- a/frontend/src/lib/components/instanceSettings.ts +++ b/frontend/src/lib/components/instanceSettings.ts @@ -836,16 +836,12 @@ export const settings: Record = { description: 'Default python version for newly deployed scripts', key: 'instance_python_version', fieldType: 'select_python', - // To change latest stable version: - // 1. Change placeholder in instanceSettings.ts - // 2. Change LATEST_STABLE_PY in dockerfile - // 3. Change #[default] annotation for PyVersion in backend placeholder: '3.10,3.11,3.12,3.13', select_items: [ { - label: 'Latest Stable', + label: 'Instance default', value: 'default', - tooltip: 'python-3.12' + tooltip: 'The version this instance was initialized with' }, { label: '3.10'