feat: open-source worker group configuration UI (#7954)

* feat: gate private registry configuration behind enterprise feature

Add read_ee_registry<T> helper that returns None and emits a job log
in CE builds when a private registry is configured. Applied to all
language executors: pip, npm/bun, deno, cargo, go, maven, nuget,
powershell, and ruby. Maven settings.xml file write in monitor.rs
is also gated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: open-source worker group configuration UI

Allow CE users to create worker group configs and edit worker_tags.
EE-only features (dedicated workers, custom env vars, init/periodic
scripts, autoscaling, priority tags, etc.) remain gated behind the
enterprise license in the UI. Backend now filters CE config updates
to only allow worker_tags and cache_clear fields instead of rejecting
the entire request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-15 14:05:23 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent a0f6243ba9
commit 1754a64cd6
3 changed files with 43 additions and 47 deletions
+10 -7
View File
@@ -23,7 +23,7 @@ use windmill_common::{
DB,
};
use windmill_api_auth::{ApiAuthed, require_devops_role};
use windmill_api_auth::{require_devops_role, ApiAuthed};
pub fn global_service() -> Router {
Router::new()
@@ -128,12 +128,15 @@ async fn update_config(
require_devops_role(&db, &authed.email).await?;
#[cfg(not(feature = "enterprise"))]
if name.starts_with("worker__") {
return Err(error::Error::BadRequest(
"Worker groups configurable from UI available only in the enterprise version"
.to_string(),
));
}
let config = if name.starts_with("worker__") {
// In CE, only allow setting worker_tags (and cache_clear for cache management)
serde_json::json!({
"worker_tags": config.get("worker_tags"),
"cache_clear": config.get("cache_clear")
})
} else {
config
};
if name.starts_with("worker__") {
let periodic_script_bash = config
+30 -31
View File
@@ -226,9 +226,9 @@
}
// Centralized permission logic
let canEditConfig = $derived($enterpriseLicense && ($superadmin || $devopsRole))
let canEditConfig = $derived($superadmin || $devopsRole)
let hasEnterpriseFeatures = $derived($enterpriseLicense)
let isReadOnly = $derived(!canEditConfig)
let canEditEEConfig = $derived(canEditConfig && hasEnterpriseFeatures)
const dispatch = createEventDispatcher()
@@ -340,12 +340,11 @@
<DrawerContent
on:close={() => drawer?.closeDrawer()}
title={canEditConfig ? `Edit worker config '${name}'` : `Worker config '${name}'`}
eeOnly
>
{#if !hasEnterpriseFeatures}
<Alert type="info" title="Worker management UI is EE only" class="mb-4">
Workers can still have their WORKER_TAGS, INIT_SCRIPT and WHITELIST_ENVS passed as env.
Dedicated workers are an enterprise only feature.
<Alert type="info" title="Enterprise features" class="mb-4">
Some worker management features require the enterprise edition. In CE, you can configure
worker tags.
</Alert>
{:else if !canEditConfig}
<Alert type="info" title="Read-only mode" class="mb-4">
@@ -355,7 +354,7 @@
<Label label="Workers assignment">
<ToggleButtonGroup
{selected}
disabled={!canEditConfig}
disabled={!canEditEEConfig}
on:selected={(e) => {
if (nconfig == undefined) {
nconfig = {}
@@ -483,7 +482,7 @@
</Tooltip>
{/snippet}
<MultiSelect
disabled={!canEditConfig}
disabled={!canEditEEConfig}
bind:value={
() => (nconfig.priority_tags ? Object.keys(nconfig.priority_tags) : []),
(v) => {
@@ -509,7 +508,7 @@
nconfig.min_alive_workers_alert_threshold = ev.detail ? 1 : undefined
}
}}
disabled={!canEditConfig}
disabled={!canEditEEConfig}
/>
{#if nconfig.min_alive_workers_alert_threshold !== undefined}
<div class="flex flex-row items-center text-xs gap-2">
@@ -517,7 +516,7 @@
<input
type="number"
class="!w-14 text-center"
disabled={!canEditConfig}
disabled={!canEditEEConfig}
min="1"
bind:value={nconfig.min_alive_workers_alert_threshold}
/>
@@ -545,7 +544,7 @@
{#if nconfig !== undefined}
<DedicatedWorkersSelector
selectedTags={nconfig.dedicated_workers ?? []}
disabled={!canEditConfig}
disabled={!canEditEEConfig}
onchange={(tags) => {
if (nconfig) {
nconfig.dedicated_workers = tags
@@ -567,14 +566,14 @@
{#each customEnvVars as envvar, i}
<div class="flex gap-1 items-center">
<input
disabled={isReadOnly}
disabled={!canEditEEConfig}
type="text"
placeholder="ENV_VAR_NAME"
bind:value={envvar.key}
onkeypress={(e) => {}}
/>
<ToggleButtonGroup
disabled={!canEditConfig}
disabled={!canEditEEConfig}
class="w-128"
bind:selected={envvar.type}
on:selected={(e) => {
@@ -584,20 +583,20 @@
}}
>
{#snippet children({ item })}
<ToggleButton value="dynamic" label="Dynamic" {item} disabled={!canEditConfig} />
<ToggleButton value="static" label="Static" {item} disabled={!canEditConfig} />
<ToggleButton value="dynamic" label="Dynamic" {item} disabled={!canEditEEConfig} />
<ToggleButton value="static" label="Static" {item} disabled={!canEditEEConfig} />
{/snippet}
</ToggleButtonGroup>
<TextInput
inputProps={{
type: 'text',
disabled: isReadOnly || envvar.type === 'dynamic',
disabled: !canEditEEConfig || envvar.type === 'dynamic',
placeholder:
envvar.type === 'dynamic' ? 'value read from worker env var' : 'static value'
}}
bind:value={envvar.value}
/>
{#if canEditConfig}
{#if canEditEEConfig}
<Button
wrapperClasses="ml-2"
variant="subtle"
@@ -618,12 +617,12 @@
startIcon={{ icon: Trash }}
iconOnly
destructive
disabled={!canEditConfig}
disabled={!canEditEEConfig}
/>
{/if}
</div>
{/each}
{#if canEditConfig}
{#if canEditEEConfig}
<div class="flex flex-col gap-2">
<Button
variant="default"
@@ -633,7 +632,7 @@
customEnvVars.push({ key: '', type: 'dynamic', value: undefined })
customEnvVars = [...customEnvVars]
}}
disabled={!canEditConfig}
disabled={!canEditEEConfig}
>
Add environment variable
</Button>
@@ -698,7 +697,7 @@
<AutoscalingConfigEditor
worker_tags={config?.worker_tags}
bind:config={nconfig.autoscaling}
disabled={!canEditConfig}
disabled={!canEditEEConfig}
/>
<div class="mt-8"></div>
@@ -713,11 +712,11 @@
<div class="flex gap-1 items-center">
<input
type="text"
disabled={!canEditConfig}
disabled={!canEditEEConfig}
placeholder="/path/to/python3.X/site-packages"
bind:value={nconfig.additional_python_paths![i]}
/>
{#if canEditConfig}
{#if canEditEEConfig}
<button
class="rounded-full bg-surface/60 hover:bg-surface-hover"
aria-label="Clear"
@@ -738,7 +737,7 @@
</div>
{/each}
{/if}
{#if canEditConfig}
{#if canEditEEConfig}
<div class="flex">
<Button
variant="default"
@@ -765,12 +764,12 @@
{#each nconfig.pip_local_dependencies as _, i}
<div class="flex gap-1 items-center">
<input
disabled={!canEditConfig}
disabled={!canEditEEConfig}
type="text"
placeholder="httpx"
bind:value={nconfig.pip_local_dependencies[i]}
/>
{#if canEditConfig}
{#if canEditEEConfig}
<button
class="rounded-full bg-surface/60 hover:bg-surface-hover"
aria-label="Clear"
@@ -791,7 +790,7 @@
</div>
{/each}
{/if}
{#if canEditConfig}
{#if canEditEEConfig}
<div class="flex">
<Button
variant="default"
@@ -819,7 +818,7 @@
tooltip="Bash scripts for worker initialization and maintenance. Init scripts run at worker start, periodic scripts run at configurable intervals."
collapsable
>
{#if canEditConfig}
{#if canEditEEConfig}
<div class="mb-4">
<Alert size="xs" type="info" title="Worker restart required">
Workers will get killed upon detecting any changes in this section (scripts or
@@ -850,7 +849,7 @@
<div class="border w-full h-40">
<Editor
fixedOverflowWidgets={true}
disabled={!canEditConfig}
disabled={!canEditEEConfig}
class="flex flex-1 grow h-full w-full"
automaticLayout
scriptLang={'bash'}
@@ -893,7 +892,7 @@
<Label label="Execution interval (seconds)" for="periodic-script-interval-seconds">
<TextInput
inputProps={{
disabled: !canEditConfig,
disabled: !canEditEEConfig,
type: 'number',
min: '60',
placeholder: '3600',
@@ -907,7 +906,7 @@
<div class="border w-full h-40">
<Editor
disabled={!canEditConfig}
disabled={!canEditEEConfig}
class="flex flex-1 grow h-full w-full"
automaticLayout
scriptLang={'bash'}
@@ -45,7 +45,6 @@
import Select from '$lib/components/select/Select.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
import TagList from '$lib/components/TagList.svelte'
import EEOnly from '$lib/components/EEOnly.svelte'
import MeltTooltip from '$lib/components/meltComponents/Tooltip.svelte'
let workers: WorkerPing[] | undefined = $state(undefined)
@@ -607,8 +606,7 @@
if (
e.key === 'Enter' &&
newConfigName &&
newConfigName.trim() !== '' &&
$enterpriseLicense
newConfigName.trim() !== ''
) {
addConfig()
}
@@ -627,7 +625,6 @@
unifiedSize="md"
startIcon={{ icon: Plus }}
nonCaptureEvent
disabled={!$enterpriseLicense}
dropdownItems={$enterpriseLicense
? [
{
@@ -657,7 +654,7 @@
: undefined}
>
<span class="hidden md:block"
>New group config {!$enterpriseLicense ? '(EE)' : ''}</span
>New group config</span
>
<Tooltip light>
@@ -674,14 +671,11 @@
bind:value={newConfigName}
/>
{#if !$enterpriseLicense}
<EEOnly />
{/if}
<Button
unifiedSize="md"
variant="accent"
startIcon={{ icon: Plus }}
disabled={!newConfigName || !$enterpriseLicense}
disabled={!newConfigName}
on:click={addConfig}
>
Create