diff --git a/README.md b/README.md index 002a9990f3..219bf7f265 100644 --- a/README.md +++ b/README.md @@ -226,11 +226,9 @@ From there, you can follow the setup app and create other users. We publish helm charts at: . -### Postgres without superuser +### OAuth, SSO & SMTP -If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a -postgres superuser, you can run `./init-db-as-superuser.sql` to init the -required users for Windmill. +Windmill Community Edition allows to configure the OAuth, SSO (including Google Workspace SSO, Microsoft/Azure and Okta) directly from the UI in the superadmin settings. Do note that there is a limit of 50 SSO users on the community edition. ### Commercial license @@ -249,76 +247,6 @@ your current infrastructure to Windmill, support with tight SLA, and our global cache sync for high-performance/no dependency cache miss of cluster from 10+ nodes to 200+ nodes. -### OAuth for self-hosting - -To get the same oauth integrations as Windmill Cloud, mount `oauth.json` with -the following format: - -```json -{ - "": { - "id": "", - "secret": "", - "allowed_domains": ["windmill.dev"] //restrict a client OAuth login to some domains - } -} -``` - -and mount it at `/usr/src/app/oauth.json`. - -The redirect url for the oauth clients is: -`/user/login_callback/` - -Even if you setup oauth, you will still want to **login as admin@windmill.dev / -changeme** to setup your instance as a super-admin and give yourself admin -rights. - -[The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json) - -To add more "connect an app" OAuth clients to the Windmill project, read the -[Contributor's guide](https://www.windmill.dev/docs/misc/contributing). We -welcome contributions! - -You may also add your own custom OAuth2 IdP and OAuth2 Resource provider: - -```json -{ - "": { - "id": "", - "secret": "", - // To add a new OAuth2 IdP - "login_config": { - "auth_url": "", - "token_url": "", - "userinfo_url": "", - "scopes": ["scope1", "scope2"], - "extra_params": "" - }, - // To add a new OAuth2 Resource - "connect_config": { - "auth_url": "", - "token_url": "", - "scopes": ["scope1", "scope2"], - "extra_params": "" - } - } -} -``` - -### smtp for self-hosting - -For users to receive emails when you invite them to workspaces or add them to -the instances using their emails, configure the SMTP env variables in the -servers: - -``` -SMTP_FROM=noreply@windmill.dev -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USERNAME=ruben@windmill.dev -SMTP_PASSWORD=yourpasswordapp -``` - ### Resource types You will also want to import all the approved resource types from @@ -330,7 +258,7 @@ it being synced automatically everyday. | Environment Variable name | Default | Description | Api Server/Worker/All | | --------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | DATABASE_URL | | The Postgres database url. | All | -| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker | +| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker | | SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server | | PORT | 8000 | Exposed port | Server | | NUM_WORKERS | 1 | The number of worker per Worker instance (Set to 0 for API/Server instances, Set to 1 for normal workers, and > 1 for workers dedicated to native jobs) | Worker | @@ -395,12 +323,13 @@ it being synced automatically everyday. | SMTP_USERNAME | None | username for the smtp server to send invite emails | Server | | SMTP_PASSWORD | None | password for the smtp server to send invite emails | Server | | SMTP_TLS_IMPLICIT | false | https://docs.rs/mail-send/latest/mail_send/struct.SmtpClientBuilder.html#method.implicit_tlsemails | Server | -| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server | +| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server | | GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script to run when a root job fails. The script will be run in and from the admins workspace | Server | | WHITELIST_ENVS | None | List of envs variables, separated by a ',' that are whitelisted as being safe to passthrough the workers | Worker | | SAML_METADATA | None | SAML Metadata URL to enable SAML SSO (EE only) | Server | | SECRET_SALT | None | Secret Salt used for encryption and decryption of secrets. If defined, the secrets will not be decryptable unless the right salt is passed in, which is the case for the workers and the server | Server + Worker | | OPENAI_AZURE_BASE_PATH | None | Azure OpenAI API base path (no trailing slash) | Server | +| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker | ## Run a local dev setup diff --git a/backend/.sqlx/query-025e5bf6aef56e9832239489daf21b4970d84529cf7ed7928ce2dcfec8b007f0.json b/backend/.sqlx/query-025e5bf6aef56e9832239489daf21b4970d84529cf7ed7928ce2dcfec8b007f0.json new file mode 100644 index 0000000000..6823a71ae0 --- /dev/null +++ b/backend/.sqlx/query-025e5bf6aef56e9832239489daf21b4970d84529cf7ed7928ce2dcfec8b007f0.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM global_settings WHERE name = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "025e5bf6aef56e9832239489daf21b4970d84529cf7ed7928ce2dcfec8b007f0" +} diff --git a/backend/migrations/20231002123723_notify_delete_config.down.sql b/backend/migrations/20231002123723_notify_delete_config.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20231002123723_notify_delete_config.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20231002123723_notify_delete_config.up.sql b/backend/migrations/20231002123723_notify_delete_config.up.sql new file mode 100644 index 0000000000..c0afcb9c0f --- /dev/null +++ b/backend/migrations/20231002123723_notify_delete_config.up.sql @@ -0,0 +1,15 @@ +-- Add up migration script here + +CREATE FUNCTION "notify_global_setting_delete" () +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('notify_global_setting_change', OLD.name::text); + RETURN OLD; +END; +$$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE TRIGGER "notify_global_setting_delete" + AFTER DELETE ON "global_settings" + FOR EACH ROW +EXECUTE FUNCTION "notify_global_setting_delete" (); + diff --git a/backend/src/main.rs b/backend/src/main.rs index e8fba9e221..7a7024cb87 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -21,7 +21,8 @@ use tokio::{ }; use windmill_common::{ global_settings::{ - BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, LICENSE_KEY_SETTING, OAUTH_SETTING, + BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, EXTRA_PIP_INDEX_URL_SETTING, + LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING, }, utils::rd_string, @@ -36,8 +37,9 @@ use windmill_worker::{ }; use crate::monitor::{ - initial_load, monitor_db, reload_base_url_setting, reload_license_key, - reload_retention_period_setting, reload_server_config, reload_worker_config, + initial_load, monitor_db, reload_base_url_setting, reload_extra_pip_index_url_setting, + reload_license_key, reload_npm_config_registry_setting, reload_retention_period_setting, + reload_server_config, reload_worker_config, }; const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version"); @@ -220,52 +222,51 @@ Windmill Community Edition {GIT_VERSION} tracing::info!("Received new pg notification: {n:?}"); match n.channel() { "notify_config_change" => { - tracing::info!("Config change detected"); + tracing::info!("Config change detected: {}", n.payload()); match n.payload() { "server" if server_mode => { - tracing::info!("Server config change detected"); reload_server_config(&db).await; }, a@ _ if worker_mode && a == format!("worker__{}", *WORKER_GROUP) => { - tracing::info!("Worker config change detected"); reload_worker_config(&db, tx.clone(), true).await; }, _ => { - () + tracing::error!("config target neither a server or a worker"); } } }, "notify_global_setting_change" => { - tracing::info!("Global setting change detected"); + tracing::info!("Global setting change detected: {}", n.payload()); match n.payload() { BASE_URL_SETTING => { - tracing::info!("Base URL setting change detected"); if let Err(e) = reload_base_url_setting(&db).await { tracing::error!(error = %e, "Could not reload base url setting"); } }, OAUTH_SETTING => { - tracing::info!("OAuth setting change detected"); if let Err(e) = reload_base_url_setting(&db).await { tracing::error!(error = %e, "Could not reload oauth setting"); } }, CUSTOM_TAGS_SETTING => { - tracing::info!("Custom tags setting change detected"); if let Err(e) = reload_custom_tags_setting(&db).await { tracing::error!(error = %e, "Could not reload custom tags setting"); } }, LICENSE_KEY_SETTING => { - tracing::info!("License Key setting change detected"); if let Err(e) = reload_license_key(&db).await { tracing::error!(error = %e, "Could not reload license key setting"); } }, RETENTION_PERIOD_SECS_SETTING => { - tracing::info!("Retention period setting change detected"); reload_retention_period_setting(&db).await }, + EXTRA_PIP_INDEX_URL_SETTING => { + reload_extra_pip_index_url_setting(&db).await + }, + NPM_CONFIG_REGISTRY_SETTING => { + reload_npm_config_registry_setting(&db).await + }, REQUEST_SIZE_LIMIT_SETTING => { tracing::info!("Request limit size change detected, killing server expecting to be restarted"); // we wait a bit randomly to avoid having all servers shutdown at same time diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index db5415b326..1206ca29fb 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -15,7 +15,8 @@ use windmill_api::{ use windmill_common::{ error, global_settings::{ - BASE_URL_SETTING, LICENSE_KEY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING, + BASE_URL_SETTING, EXTRA_PIP_INDEX_URL_SETTING, LICENSE_KEY_SETTING, + NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING, }, jobs::{JobKind, QueuedJob}, @@ -25,7 +26,8 @@ use windmill_common::{ BASE_URL, DB, METRICS_ENABLED, }; use windmill_worker::{ - create_token_for_owner, handle_job_error, AuthedClient, SCRIPT_TOKEN_EXPIRY, + create_token_for_owner, handle_job_error, AuthedClient, NPM_CONFIG_REGISTRY, + PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY, }; #[cfg(feature = "enterprise")] @@ -118,6 +120,18 @@ pub async fn initial_load( } }; + let reload_extra_pip_index_url_f = async { + if worker_mode { + reload_extra_pip_index_url_setting(&db).await; + } + }; + + let reload_npm_config_registry_f = async { + if worker_mode { + reload_npm_config_registry_setting(&db).await; + } + }; + join!( reload_worker_config_f, reload_server_config_f, @@ -125,7 +139,9 @@ pub async fn initial_load( reload_request_size_f, reload_base_url_f, reload_retention_period_f, - reload_license_key_f + reload_license_key_f, + reload_extra_pip_index_url_f, + reload_npm_config_registry_f ); } @@ -201,6 +217,32 @@ pub async fn delete_expired_items(db: &DB) -> () { } } +pub async fn reload_extra_pip_index_url_setting(db: &DB) { + if let Err(e) = reload_option_string_setting( + db, + EXTRA_PIP_INDEX_URL_SETTING, + "PIP_EXTRA_INDEX_URL", + PIP_EXTRA_INDEX_URL.clone(), + ) + .await + { + tracing::error!("Error reloading extra_pip_index_url period: {:?}", e) + } +} + +pub async fn reload_npm_config_registry_setting(db: &DB) { + if let Err(e) = reload_option_string_setting( + db, + NPM_CONFIG_REGISTRY_SETTING, + "NPM_CONFIG_REGISTRY", + NPM_CONFIG_REGISTRY.clone(), + ) + .await + { + tracing::error!("Error reloading npm_config_registry period: {:?}", e) + } +} + pub async fn reload_retention_period_setting(db: &DB) { if let Err(e) = reload_setting( db, @@ -261,6 +303,44 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> { Ok(()) } +pub async fn reload_option_string_setting( + db: &DB, + setting_name: &str, + std_env_var: &str, + lock: Arc>>, +) -> error::Result<()> { + let q = sqlx::query!( + "SELECT value FROM global_settings WHERE name = $1", + setting_name + ) + .fetch_optional(db) + .await?; + + let mut value = std::env::var(std_env_var).ok(); + + if let Some(q) = q { + if let Ok(v) = serde_json::from_value::(q.value.clone()) { + tracing::info!( + "Loaded setting {setting_name} from db config: {:#?}", + &q.value + ); + value = Some(v) + } else { + tracing::error!("Could not parse {setting_name} found: {:#?}", &q.value); + } + }; + + { + if value.is_none() { + tracing::info!("Loaded {setting_name} setting to None"); + } + let mut l = lock.write().await; + *l = value; + } + + Ok(()) +} + pub async fn reload_setting( db: &DB, setting_name: &str, diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 99da96777c..882e93dbde 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -79,7 +79,6 @@ pub struct TestKey { pub license_key: String, } - pub async fn test_license_key( Extension(db): Extension, authed: ApiAuthed, @@ -109,6 +108,14 @@ pub async fn get_local_settings( pub struct Value { pub value: serde_json::Value, } + +pub async fn delete_global_setting(db: &DB, key: &str) -> error::Result<()> { + sqlx::query!("DELETE FROM global_settings WHERE name = $1", key,) + .execute(db) + .await?; + tracing::info!("Unset global setting {}", key); + Ok(()) +} pub async fn set_global_setting( Extension(db): Extension, authed: ApiAuthed, @@ -116,14 +123,25 @@ pub async fn set_global_setting( Json(value): Json, ) -> error::Result<()> { require_super_admin(&db, &authed.email).await?; - sqlx::query!( - "INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()", - key, - value.value - ) - .execute(&db) - .await?; - tracing::info!("Set global setting {} to {}", key, value.value); + match value.value { + serde_json::Value::Null => { + delete_global_setting(&db, &key).await?; + } + serde_json::Value::String(x) if x.is_empty() => { + delete_global_setting(&db, &key).await?; + } + v => { + sqlx::query!( + "INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()", + key, + v + ) + .execute(&db) + .await?; + tracing::info!("Set global setting {} to {}", key, v); + } + }; + Ok(()) } diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 8119f783bd..9fee7b0aac 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -5,6 +5,8 @@ pub const OAUTH_SETTING: &str = "oauths"; pub const RETENTION_PERIOD_SECS_SETTING: &str = "retention_period_secs"; pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb"; pub const LICENSE_KEY_SETTING: &str = "license_key"; +pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry"; +pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url"; pub const ENV_SETTINGS: [&str; 54] = [ "DISABLE_NSJAIL", diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index 4fe282c064..3e67382a85 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -93,7 +93,7 @@ pub async fn gen_lockfile( .await?; let common_bun_proc_envs: HashMap = - get_common_bun_proc_envs(&base_internal_url); + get_common_bun_proc_envs(&base_internal_url).await; let child = Command::new(&*BUN_PATH) .current_dir(job_dir) @@ -252,7 +252,7 @@ pub async fn handle_bun_job( let _ = write_file(job_dir, "main.ts", inner_content).await?; let common_bun_proc_envs: HashMap = - get_common_bun_proc_envs(&base_internal_url); + get_common_bun_proc_envs(&base_internal_url).await; if let Some(reqs) = requirements_o { let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::>(); @@ -297,7 +297,8 @@ pub async fn handle_bun_job( // TODO: remove once bun implement a reasonable set of trusted deps let trusted_deps = get_trusted_deps(inner_content); let empty_trusted_deps = trusted_deps.len() == 0; - if !*DISABLE_NSJAIL || !empty_trusted_deps { + let has_custom_config_registry = common_bun_proc_envs.contains_key("NPM_CONFIG_REGISTRY"); + if !*DISABLE_NSJAIL || !empty_trusted_deps || has_custom_config_registry { logs.push_str("\n\n--- BUN INSTALL ---\n"); set_logs(&logs, &job.id, &db).await; let _ = gen_lockfile( @@ -316,7 +317,7 @@ pub async fn handle_bun_job( .await?; } - if empty_trusted_deps { + if empty_trusted_deps && !has_custom_config_registry { let node_modules_path = format!("{}/node_modules", job_dir); let node_modules_exists = tokio::fs::metadata(&node_modules_path).await.is_ok(); if node_modules_exists { @@ -498,8 +499,8 @@ plugin(p) read_result(job_dir).await } -pub fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap { - let mut deno_envs: HashMap = HashMap::from([ +pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap { + let mut bun_envs: HashMap = HashMap::from([ (String::from("PATH"), PATH_ENV.clone()), (String::from("HOME"), HOME_ENV.clone()), (String::from("TZ"), TZ_ENV.clone()), @@ -517,10 +518,10 @@ pub fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap = - get_common_bun_proc_envs(&base_internal_url); + get_common_bun_proc_envs(&base_internal_url).await; let context = variables::get_reserved_variables( w_id, &token, diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index faf7a5e043..377a120857 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -63,7 +63,7 @@ async fn get_common_deno_proc_envs( ), ]); - if let Some(ref s) = *NPM_CONFIG_REGISTRY { + if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() { deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone()); } return deno_envs; @@ -92,6 +92,10 @@ pub async fn generate_deno_lock( write_file(job_dir, "import_map.json", &import_map).await?; write_file(job_dir, "empty.ts", "").await?; + let mut deno_envs = HashMap::new(); + if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() { + deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone()); + } let child = Command::new(DENO_PATH.as_str()) .current_dir(job_dir) .args(vec![ @@ -103,6 +107,7 @@ pub async fn generate_deno_lock( &import_map_path, "main.ts", ]) + .envs(deno_envs) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 101487d550..975b73d173 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -25,7 +25,6 @@ lazy_static::lazy_static! { static ref PIP_INDEX_URL: Option = std::env::var("PIP_INDEX_URL").ok(); - static ref PIP_EXTRA_INDEX_URL: Option = std::env::var("PIP_EXTRA_INDEX_URL").ok(); static ref PIP_TRUSTED_HOST: Option = std::env::var("PIP_TRUSTED_HOST").ok(); static ref PIP_LOCAL_DEPENDENCIES: Option> = { let pip_local_dependencies = std::env::var("PIP_LOCAL_DEPENDENCIES") @@ -62,7 +61,7 @@ use crate::{ write_file, }, AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HTTPS_PROXY, HTTP_PROXY, - LOCK_CACHE_DIR, NO_PROXY, NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, TZ_ENV, + LOCK_CACHE_DIR, NO_PROXY, NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, PIP_EXTRA_INDEX_URL, TZ_ENV, }; pub async fn create_dependencies_dir(job_dir: &str) { @@ -117,7 +116,8 @@ pub async fn pip_compile( write_file(job_dir, file, &requirements).await?; let mut args = vec!["-q", "--no-header", file, "--resolver=backtracking"]; - if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() { + let pip_extra_index_url = PIP_EXTRA_INDEX_URL.read().await.clone(); + if let Some(url) = pip_extra_index_url.as_ref() { args.extend(["--extra-index-url", url]); } if let Some(url) = PIP_INDEX_URL.as_ref() { @@ -497,8 +497,11 @@ pub async fn handle_python_reqs( ) -> error::Result> { let mut req_paths: Vec = vec![]; let mut vars = vec![("PATH", PATH_ENV.as_str())]; + let pip_extra_index_url; + if !*DISABLE_NSJAIL { - if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() { + pip_extra_index_url = PIP_EXTRA_INDEX_URL.read().await.clone(); + if let Some(url) = pip_extra_index_url.as_ref() { vars.push(("EXTRA_INDEX_URL", url)); } if let Some(url) = PIP_INDEX_URL.as_ref() { @@ -595,7 +598,8 @@ pub async fn handle_python_reqs( "-t", venv_p.as_str(), ]; - if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() { + let pip_extra_index_url = PIP_EXTRA_INDEX_URL.read().await.clone(); + if let Some(url) = pip_extra_index_url.as_ref() { command_args.extend(["--extra-index-url", url]); } if let Some(url) = PIP_INDEX_URL.as_ref() { diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 1cf0316a3d..47c6176e75 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -223,7 +223,8 @@ lazy_static::lazy_static! { pub static ref NETRC: Option = std::env::var("NETRC").ok(); - pub static ref NPM_CONFIG_REGISTRY: Option = std::env::var("NPM_CONFIG_REGISTRY").ok(); + pub static ref NPM_CONFIG_REGISTRY: Arc>> = Arc::new(RwLock::new(None)); + pub static ref PIP_EXTRA_INDEX_URL: Arc>> = Arc::new(RwLock::new(None)); diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index 5b4832a08d..a68b4dba8e 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -17,6 +17,7 @@ import { capitalize } from '$lib/utils' import { enterpriseLicense } from '$lib/stores' import CustomOauth from './CustomOauth.svelte' + import { AlertTriangle } from 'lucide-svelte' export const settings: Record = { Core: [ @@ -61,6 +62,26 @@ fieldType: 'license_key', placeholder: 'only needed to prepare upgrade to EE', storage: 'setting' + }, + { + label: 'Pip Extra Index Url', + description: 'Add private PIP registry', + key: 'pip_extra_index_url', + fieldType: 'text', + placeholder: 'https://username:password@pypi.company.com/simple', + storage: 'setting', + ee_only: + 'You can still set this setting by using PIP_EXTRA_INDEX_URL as env variable to the worker containers' + }, + { + label: 'Npm Config Registry', + description: 'Add private NPM registry', + key: 'npm_config_registry', + fieldType: 'text', + placeholder: 'https://yourregistry', + storage: 'setting', + ee_only: + 'You can still set this setting by using NPM_CONFIG_REGISTRY as env variable to the worker containers' } ], SMTP: [ @@ -153,7 +174,7 @@ const allSettings = Object.values(settings).flatMap((x) => Object.entries(x)) const newServerConfig = Object.fromEntries( allSettings - .filter((x) => x[1].storage == 'config') + .filter((x) => x[1].storage == 'config' && values?.[x[1].key] && values?.[x[1].key] != '') .map((x) => [x[1].key, values?.[x[1].key]]) ) if (!deepEqual(newServerConfig, serverConfig)) { @@ -165,19 +186,20 @@ } await Promise.all( allSettings - .filter( - (x) => + .filter((x) => { + return ( x[1].storage == 'setting' && !deepEqual(initialValues?.[x[1].key], values?.[x[1].key]) && - values?.[x[1].key] != undefined && - values?.[x[1].key] != null && - values?.[x[1].key] != '' - ) + (values?.[x[1].key] != '' || + initialValues?.[x[1].key] != undefined || + initialValues?.[x[1].key] != null) + ) + }) .map(async ([_, x]) => { await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } }) }) ) - initialValues = JSON.parse(JSON.stringify(initialValues)) + initialValues = JSON.parse(JSON.stringify(values)) if (!deepEqual(initialOauths, oauths)) { await SettingService.setGlobal({ @@ -251,6 +273,12 @@
{#each settings[category] as setting} {#if !setting.cloudonly || isCloudHosted()} + {#if setting.ee_only != undefined && !$enterpriseLicense} +
+ + EE only {setting.ee_only} +
+ {/if}