mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
* 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>
349 lines
9.5 KiB
Markdown
349 lines
9.5 KiB
Markdown
# 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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
# 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_key`
|
|
- `hub_api_secret`
|
|
- `scim_token`
|
|
- `smtp_settings.smtp_password`
|
|
- `oauths.<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
|
|
|
|
1. Windmill reads and parses the YAML file
|
|
2. Any `envRef` fields are resolved from the container's environment variables
|
|
3. The current database state is read
|
|
4. A diff is computed (using `Replace` mode: settings absent from the file are deleted, except protected ones like `ducklake_settings`)
|
|
5. Changes are applied to the database
|
|
|
|
### Setup
|
|
|
|
See the included [`docker-compose.yml`](docker-compose.yml) for a complete working example. The key parts:
|
|
|
|
**1. Create your config file** (`windmill-config.yaml`):
|
|
|
|
```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`**:
|
|
|
|
```yaml
|
|
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):
|
|
|
|
```env
|
|
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:
|
|
|
|
```bash
|
|
docker compose run --rm windmill_config_sync
|
|
```
|
|
|
|
Or, for CI/CD pipelines, run the binary directly:
|
|
|
|
```bash
|
|
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 `operator` feature flag
|
|
- RBAC permissions for the operator pod (see below)
|
|
- The CRD installed in the cluster
|
|
|
|
### Setup
|
|
|
|
**1. Install the CRD**:
|
|
|
|
```bash
|
|
windmill operator crd | kubectl apply -f -
|
|
```
|
|
|
|
**2. Create a Kubernetes Secret for sensitive values**:
|
|
|
|
```yaml
|
|
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`):
|
|
|
|
```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**:
|
|
|
|
```bash
|
|
kubectl apply -f windmill-instance.yaml
|
|
```
|
|
|
|
**5. Check status**:
|
|
|
|
```bash
|
|
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):
|
|
|
|
```yaml
|
|
spec:
|
|
global_settings:
|
|
license_key:
|
|
envRef: "WM_LICENSE_KEY" # Read from operator pod env
|
|
```
|
|
|
|
The operator pod's Deployment would include:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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.
|