* Add windmill-operator crate for Kubernetes CRD-based instance config Introduces a new `windmill-operator` crate that enables declarative management of Windmill instance configuration via a Kubernetes `WindmillInstance` CRD. The operator watches CRD resources and performs full declarative sync of global_settings and worker configs to the database, supporting GitOps workflows for instance-level configuration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add tests for windmill-operator CRD and db_sync - 9 unit tests for CRD serialization, deserialization, metadata, and status field behavior - 15 integration tests for db_sync using #[sqlx::test] with full declarative sync coverage: upsert, delete, protected keys, idempotency, worker config prefix handling, and end-to-end sync Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace untyped BTreeMap CRD fields with typed structs for schema validation GlobalSettings, SmtpSettings, IndexerSettings, and WorkerGroupConfig now have explicit typed fields with serde(flatten) catch-all for forward compatibility. The generated CRD YAML includes a full OpenAPI v3 schema that Kubernetes validates on kubectl apply. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Type opaque serde_json::Value CRD fields with real structs Replace most remaining serde_json::Value fields in WindmillInstance CRD with properly typed structs derived from the codebase: - oauths: BTreeMap<String, OAuthClient> - otel: OtelSettings - otel_tracing_proxy: OtelTracingProxySettings with ScriptLang enum - critical_error_channels: Vec<CriticalErrorChannel> (untagged enum) - critical_alerts_on_db_oversize: DbOversizeAlert - ducklake_settings: DucklakeSettings with nested catalog/storage types - custom_instance_pg_databases: CustomInstancePgDatabases - autoscaling (worker config): AutoscalingConfig with integration struct - custom_tags, default_tags_workspaces: Vec<String> - default_tags_per_workspace: bool Still opaque (serde_json::Value): object_store_cache_config (kube-core can't generate schemas for internally-tagged enums), secret_backend (EE-private), slack, teams (no clear struct definitions). Regenerated CRD YAML with full OpenAPI schema (352→703 lines). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Unify instance config types and add bulk GET/PUT API Move all typed settings (GlobalSettings, WorkerGroupConfig, etc.) from windmill-operator/crd.rs into windmill-common/instance_config.rs so both the API server and operator share a single source of truth. Add diff/apply logic (Merge mode for UI, Replace mode for operator) and InstanceConfig::from_db(). Add GET/PUT /settings/instance_config endpoints so the frontend loads all settings in 1 call instead of 42, and saves with a single bulk PUT. The backend handles the diff internally, running pre-write hooks for changed keys. Refactor windmill-operator/db_sync.rs to use the shared diff+apply functions and slim crd.rs down to the CRD wrapper with re-exports. Includes 32 unit tests and 30 integration tests covering serialization, diff logic, DB roundtrips, protected settings, and edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Form/YAML toggle to instance settings UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: show Form/YAML toggle regardless of hideTabs prop Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: replace toggle button group with simple YAML toggle Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: mask sensitive fields in YAML view with show/hide toggle Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: hide internal settings and mask sensitive fields in YAML view Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: hide jwt_secret and min_keep_alive_version from API and config exports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * all * feat: add secretKeyRef support for sensitive fields in operator CRD Allow sensitive fields (license_key, hub_api_secret, scim_token, smtp_password, OAuthClient.secret, custom PG user_pwd) to reference Kubernetes Secrets via the standard secretKeyRef pattern instead of inlining values as plaintext YAML. The reconciler resolves all refs by reading K8s Secrets before syncing to the database. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * all * all * fix: merge main and update dev environment docs Resolve merge conflicts from origin/main, fix duplicate UV_INDEX_STRATEGY_SETTING import, and add Playwright MCP testing instructions to CLAUDE.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all * fix: init tracing for CLI subcommands and deduplicate setting side-effects Initialize tracing subscriber before early-return CLI paths (sync-config, operator) so tracing calls are not silently dropped. Refactor set_global_setting_internal to call run_setting_pre_write_hook instead of duplicating the side-effect logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add `wmill instance get-config` CLI command Dumps the current instance config (global settings + worker configs) as YAML. Supports --output-file to write to a file instead of stdout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * all --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Windmill Instance Configuration as Code
Windmill supports managing instance configuration (global settings + worker group configs) declaratively through YAML files. This enables Infrastructure-as-Code (IaC) workflows where your Windmill instance settings are version-controlled and applied automatically.
Two deployment models are supported:
| Approach | Best for | Requires |
|---|---|---|
sync-config CLI |
Docker Compose, VMs, CI/CD pipelines | Database access |
| Kubernetes Operator | Kubernetes clusters | operator feature flag, RBAC |
Both use the same YAML schema (InstanceConfig) and the same secret reference mechanisms.
Config File Reference
A Windmill instance config file has two top-level keys:
global_settings:
# Instance-wide settings (stored in the global_settings table)
base_url: "https://windmill.example.com"
retention_period_secs: 2592000
# ...
worker_configs:
# Worker group configurations (stored in the config table as worker__<name>)
default:
worker_tags: ["deno", "python3", "bun", "go", "bash"]
gpu:
dedicated_worker: "ws:f/gpu_inference"
# ...
All fields are optional. Only the fields you specify are synced to the database.
Sensitive Field References
Fields that contain secrets (license keys, OAuth secrets, SMTP passwords, etc.) support three formats:
# 1. Plain literal (not recommended for production)
license_key: "my-license-key"
# 2. Environment variable reference (works everywhere)
license_key:
envRef: "WM_LICENSE_KEY"
# 3. Kubernetes Secret reference (K8s only)
license_key:
secretKeyRef:
name: windmill-secrets # Secret resource name
key: license-key # Key within the Secret
Fields that support envRef and secretKeyRef:
license_keyhub_api_secretscim_tokensmtp_settings.smtp_passwordoauths.<provider>.secret(each OAuth client secret)custom_instance_pg_databases.user_pwd
Docker Compose (sync-config)
The sync-config subcommand reads a YAML config file, resolves any envRef references from the process environment, and syncs the result to the database.
How it works
- Windmill reads and parses the YAML file
- Any
envReffields are resolved from the container's environment variables - The current database state is read
- A diff is computed (using
Replacemode: settings absent from the file are deleted, except protected ones likeducklake_settings) - Changes are applied to the database
Setup
See the included docker-compose.yml for a complete working example. The key parts:
1. Create your config file (windmill-config.yaml):
global_settings:
base_url: "https://windmill.example.com"
license_key:
envRef: "WM_LICENSE_KEY"
retention_period_secs: 2592000
expose_metrics: true
smtp_settings:
smtp_host: "smtp.example.com"
smtp_port: 587
smtp_from: "windmill@example.com"
smtp_password:
envRef: "SMTP_PASSWORD"
oauths:
google:
id: "google-client-id"
secret:
envRef: "GOOGLE_OAUTH_SECRET"
login_config:
auth_url: "https://accounts.google.com/o/oauth2/v2/auth"
token_url: "https://oauth2.googleapis.com/token"
userinfo_url: "https://openidconnect.googleapis.com/v1/userinfo"
scopes: ["openid", "profile", "email"]
custom_tags:
- gpu
- high-mem
worker_configs:
default:
worker_tags: ["deno", "python3", "bun", "go", "bash", "powershell"]
init_bash: "echo 'Worker starting'"
native:
worker_tags: ["nativets"]
2. Add an init container to docker-compose.yml:
services:
windmill_config_sync:
image: ${WM_IMAGE}
# Run once at startup then exit
restart: "no"
command: ["windmill", "sync-config", "/config/windmill-config.yaml"]
environment:
- DATABASE_URL=${DATABASE_URL}
- WM_LICENSE_KEY=${WM_LICENSE_KEY}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- GOOGLE_OAUTH_SECRET=${GOOGLE_OAUTH_SECRET}
volumes:
- ./windmill-config.yaml:/config/windmill-config.yaml:ro
depends_on:
db:
condition: service_healthy
3. Set secrets in your .env file (not committed to version control):
DATABASE_URL=postgres://postgres:changeme@db/windmill
WM_IMAGE=ghcr.io/windmill-labs/windmill-ee:main
WM_LICENSE_KEY=your-license-key-here
SMTP_PASSWORD=your-smtp-password
GOOGLE_OAUTH_SECRET=your-google-oauth-secret
Re-syncing after config changes
The sync-config container runs once and exits. To re-apply after editing the YAML:
docker compose run --rm windmill_config_sync
Or, for CI/CD pipelines, run the binary directly:
windmill sync-config ./windmill-config.yaml
Replace semantics
sync-config uses Replace mode: any global setting present in the database but absent from your YAML file will be deleted (except protected settings like ducklake_settings and custom_instance_pg_databases). This ensures the database state matches the file exactly.
If you only want to manage a subset of settings, include all settings you want to keep in the YAML file.
Kubernetes (Operator)
The Windmill Kubernetes operator watches WindmillInstance Custom Resources and continuously reconciles the database to match the declared state. It also supports secretKeyRef to pull values from Kubernetes Secrets natively.
Prerequisites
- Windmill built with the
operatorfeature flag - RBAC permissions for the operator pod (see below)
- The CRD installed in the cluster
Setup
1. Install the CRD:
windmill operator crd | kubectl apply -f -
2. Create a Kubernetes Secret for sensitive values:
apiVersion: v1
kind: Secret
metadata:
name: windmill-secrets
namespace: windmill
type: Opaque
stringData:
license-key: "your-license-key-here"
smtp-password: "your-smtp-password"
google-oauth-secret: "your-google-oauth-secret"
3. Create the WindmillInstance resource (windmill-instance.yaml):
apiVersion: windmill.dev/v1alpha1
kind: WindmillInstance
metadata:
name: production
namespace: windmill
spec:
global_settings:
base_url: "https://windmill.example.com"
license_key:
secretKeyRef:
name: windmill-secrets
key: license-key
retention_period_secs: 2592000
expose_metrics: true
smtp_settings:
smtp_host: "smtp.example.com"
smtp_port: 587
smtp_from: "windmill@example.com"
smtp_password:
secretKeyRef:
name: windmill-secrets
key: smtp-password
oauths:
google:
id: "google-client-id"
secret:
secretKeyRef:
name: windmill-secrets
key: google-oauth-secret
login_config:
auth_url: "https://accounts.google.com/o/oauth2/v2/auth"
token_url: "https://oauth2.googleapis.com/token"
userinfo_url: "https://openidconnect.googleapis.com/v1/userinfo"
scopes: ["openid", "profile", "email"]
custom_tags:
- gpu
- high-mem
worker_configs:
default:
worker_tags: ["deno", "python3", "bun", "go", "bash", "powershell"]
init_bash: "echo 'Worker starting'"
native:
worker_tags: ["nativets"]
4. Apply:
kubectl apply -f windmill-instance.yaml
5. Check status:
kubectl get wmi
# NAME SYNCED LAST SYNCED AGE
# production true 2025-01-15T10:30:00Z 2d
Using envRef in Kubernetes
envRef also works in the operator context. Values are resolved from the operator pod's environment. This is useful when secrets are injected via pod env vars (e.g., from a vault sidecar):
spec:
global_settings:
license_key:
envRef: "WM_LICENSE_KEY" # Read from operator pod env
The operator pod's Deployment would include:
env:
- name: WM_LICENSE_KEY
valueFrom:
secretKeyRef:
name: windmill-secrets
key: license-key
This is functionally equivalent to using secretKeyRef directly in the CRD, but lets you use any secret injection mechanism your cluster supports (external-secrets, vault-agent, etc.).
RBAC
The operator pod needs permissions to read Secrets and manage the CRD. Minimal ClusterRole:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: windmill-operator
rules:
- apiGroups: ["windmill.dev"]
resources: ["windmillinstances", "windmillinstances/status"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
Running the operator
# As a standalone process (for development)
DATABASE_URL=postgres://... windmill operator
# In production, deploy as a Kubernetes Deployment
Choosing Between envRef and secretKeyRef
| Feature | envRef |
secretKeyRef |
|---|---|---|
| Works in Docker Compose | Yes | No |
| Works in Kubernetes | Yes | Yes |
| Works with vault sidecars | Yes | No (use envRef instead) |
| Reads from | Process environment | K8s Secrets API |
| Requires RBAC for Secrets | No | Yes |
Recommendation: Use envRef for portability across deployment targets. Use secretKeyRef when you want direct Kubernetes-native secret binding without intermediate env vars.
Full Settings Reference
For a complete list of available settings fields, generate the CRD schema:
windmill operator crd
The CRD's OpenAPI schema documents every field, its type, and whether it's optional. The same schema applies to sync-config YAML files.