Files
windmill/frontend/src/lib/components/AutoscalingConfigEditor.svelte
T

424 lines
12 KiB
Svelte

<script lang="ts">
import type { AutoscalingConfig } from './worker_group'
import Toggle from './Toggle.svelte'
import Section from './Section.svelte'
import Tooltip from './Tooltip.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { Button } from './common'
import { ExternalLink } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Label from './Label.svelte'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
import { ConfigService } from '$lib/gen'
interface Props {
config: AutoscalingConfig | undefined
worker_tags: string[] | undefined
}
let { config = $bindable(), worker_tags }: Props = $props()
const dispatch = createEventDispatcher()
let test_input: number = $state(3)
let healthCheckLoading: boolean = $state(false)
let healthCheckResult: { success: boolean; error?: string } | null = $state(null)
async function checkKubernetesHealth() {
if (!config?.integration || config.integration.type !== 'kubernetes') return
healthCheckLoading = true
healthCheckResult = null
try {
await ConfigService.nativeKubernetesAutoscalingHealthcheck()
healthCheckResult = { success: true }
} catch (error: any) {
healthCheckResult = {
success: false,
error: error.body || error.message || 'Unknown error'
}
} finally {
healthCheckLoading = false
}
}
</script>
<div class="flex flex-row gap-16 pt-2">
<div class="space-y-4 flex flex-col gap-1 max-w-xs text-sm">
<h5>Rules</h5>
<Toggle
checked={config?.enabled ?? false}
options={{ right: 'Enabled' }}
on:change={(e) => {
dispatch('dirty')
if (e.detail) {
if (!config) {
config = {
enabled: true,
min_workers: 3,
max_workers: 10,
integration: { type: 'dryrun' }
}
} else {
config.enabled = true
}
} else {
config = {
...(config ?? {
min_workers: 3,
max_workers: 10,
integration: { type: 'dryrun' }
}),
enabled: false
}
}
}}
/>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Min # of Workers
{#if config !== undefined}
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
{#if config.min_workers !== undefined && config.min_workers != undefined && config.min_workers > config.max_workers}
<div class="text-red-600 text-xs whitespace-nowrap"
>Minimum cannot be {'>'} to Maximum</div
>
{/if}
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Max # of Workers
{#if config !== undefined}
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
{:else}
<input type="number" disabled />
{/if}
</label>
<div class="p-2">
<Section label="Advanced" small collapsable={true}>
<div class="flex flex-col gap-2 text-2xs">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after an incremental scale-in/out
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
placeholder="300"
bind:value={config.cooldown_seconds}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after a full scale out
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
placeholder="1500"
bind:value={config.full_scale_cooldown_seconds}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger an incremental scale-out
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
bind:value={config.inc_scale_num_jobs_waiting}
placeholder="1"
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger a full scale out <Tooltip
>Default: max_workers, full scale out = scale out to max workers</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
placeholder="max workers"
bind:value={config.full_scale_jobs_waiting}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate % threshold to go below to trigger a scale-in (decrease) <Tooltip
>Default: 25%, need to go below average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
max="100"
placeholder="25"
bind:value={config.dec_scale_occupancy_rate}
/>
{:else}
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate threshold to exceed to trigger an incremental scale-out (increase) <Tooltip
>Default: 75%, need to exceed average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
max="100"
placeholder="75"
bind:value={config.inc_scale_occupancy_rate}
/>
{:else}
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num workers to scale-in/out by when incremental <Tooltip
>Default: (max_workers - min_workers) / 5</Tooltip
>
{#if config !== undefined}
<input
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="1"
placeholder="(max_workers - min_workers) / 5"
bind:value={config.inc_num_workers}
/>
{:else}
<input type="number" disabled />
{/if}
</label>
<Label label="Custom tags to autoscale on">
{#snippet header()}
<Tooltip>
By default, autoscaling will apply to the tags the worker group is assigned to but
you can override this here.
</Tooltip>
{/snippet}
{#if config}
{#if config.custom_tags}
<MultiSelect
bind:value={
() => config?.custom_tags ?? [],
(v) => {
config && (config.custom_tags = v.length ? v : undefined)
dispatch('dirty')
}
}
items={safeSelectItems(worker_tags)}
placeholder="Tags"
/>
{:else}
<Button
color="light"
size="xs"
variant="contained"
on:click={() => {
if (config) {
config.custom_tags = []
dispatch('dirty')
}
}}>Add custom tags</Button
>
{/if}
{/if}
</Label>
</div>
</Section>
</div>
</div>
<div class="flex flex-col gap-1 max-w-xs text-sm">
<h5>Integration</h5>
{#if config?.integration}
<ToggleButtonGroup
on:selected={(e) => dispatch('dirty')}
bind:selected={config.integration.type}
class="mb-4 mt-2"
>
{#snippet children({ item })}
<ToggleButton
value="dryrun"
label="Dry run"
tooltip="See autoscaling events but not actual scaling actions will be performed"
{item}
/>
<ToggleButton
value="script"
label="Custom script"
tooltip="Run a custom script to scale your worker group"
{item}
/>
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes" {item} />
{/snippet}
</ToggleButtonGroup>
{#if config.integration.type === 'script'}
<label>
Script path on the 'admins' workspace
<input
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.path}
/>
</label>
<label>
Custom tag for executing script (optional)
{#if config.integration.tag}
<input
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.tag}
/>
{:else}
<Button
color="light"
size="xs"
variant="contained"
on:click={() => {
if (config?.integration?.type === 'script') {
config.integration.tag = 'bash'
dispatch('dirty')
}
}}>Set tag</Button
>
{/if}
</label>
<div class="flex mt-6 gap-2">
<Button
variant="accent"
target="_blank"
endIcon={{ icon: ExternalLink }}
href="/scripts/add?hub=hub%2F9204%2Fhelper%2FScale%20a%20worker%20group%20deployed%20as%20a%20kubernetes%20service&workspace=admins"
>Create from template</Button
>
<Button
variant="accent"
target="_blank"
href={`/runs/${config.integration.path}?workspace=admins`}
endIcon={{ icon: ExternalLink }}
>
See jobs
</Button>
</div>
<div class="flex flex-row gap-2 mt-4">
<Button color="light" size="xs" variant="contained">Test scaling</Button>
<div class="flex text-xs flex-row gap-2 items-center">
<input class="!w-16" type="number" bind:value={test_input} />
workers
</div>
</div>
{/if}
{#if config.integration.type === 'kubernetes'}
<div class="text-sm text-secondary mb-3">
Kubernetes configuration is automatically inferred from the cluster environment. The
worker group name and namespace will be detected automatically.
</div>
<div class="flex flex-col gap-3 mt-4">
<div class="flex items-center gap-2">
<Button
size="xs"
variant="accent"
startIcon={{ icon: ExternalLink }}
href="https://windmill.dev/docs/core_concepts/autoscaling#kubernetes"
target="_blank"
>
Setup Guide (Roles & Bindings)
</Button>
<Button
color="light"
size="xs"
variant="contained"
onclick={checkKubernetesHealth}
disabled={healthCheckLoading}
>
{healthCheckLoading ? 'Checking...' : 'Check Health'}
</Button>
</div>
{#if healthCheckResult !== null}
<div
class="p-2 rounded-md text-sm {healthCheckResult.success
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400'}"
>
{#if healthCheckResult.success}
Kubernetes autoscaling is healthy
{:else}
{healthCheckResult.error}
{#if healthCheckResult.error?.includes('permissions') || healthCheckResult.error?.includes('role')}
<br /><small
>Please follow the setup guide above to configure proper RBAC permissions.</small
>
{/if}
{/if}
</div>
{/if}
<div class="flex flex-row gap-2">
<Button color="light" size="xs" variant="contained">Test scaling</Button>
<div class="flex text-xs flex-row gap-2 items-center">
<input class="!w-16" type="number" bind:value={test_input} />
workers
</div>
</div>
</div>
{/if}
{:else}
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
{#snippet children({ item })}
<ToggleButton value="dryrun" label="Dry run" {item} />
<ToggleButton value="script" label="Custom script" {item} />
<ToggleButton value="ecs" label="ECS (soon)" {item} />
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes" {item} />
{/snippet}
</ToggleButtonGroup>
<label>
Script path on the 'admins' workspace
<input type="text" disabled />
</label>
{/if}
</div>
</div>