diff --git a/backend/Cargo.lock b/backend/Cargo.lock index aa14a38c33..be09ae8b76 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -16294,6 +16294,7 @@ dependencies = [ "rsa", "serde", "serde_json", + "serde_yml", "sha2 0.10.9", "sqlx", "tokio", @@ -16687,11 +16688,9 @@ name = "windmill-operator" version = "1.636.0" dependencies = [ "anyhow", - "chrono", "futures", "k8s-openapi", "kube", - "schemars 0.8.22", "serde", "serde_json", "serde_yml", diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index e95062a9c0..e9b23de35c 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -06207a2083383e0320da287bdf0268454db515de \ No newline at end of file +82633d56b2db33ce8fd9ecfda31867fefb4f4823 \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index ed5c155404..3304f0c9df 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -485,8 +485,7 @@ fn print_help() { println!(" cache [hubPaths.json] Pre-cache hub scripts (default: ./hubPaths.json)"); println!(" cache-rt Pre-cache hub resource types"); println!(" sync-config Sync instance config from a YAML file to the database"); - println!(" operator Run the Kubernetes operator (watches WindmillInstance CRDs)"); - println!(" operator crd Print the WindmillInstance CRD YAML to stdout"); + println!(" operator Run the Kubernetes operator (watches a ConfigMap)"); println!(); println!("Environment variables (name = default):"); println!(" DATABASE_URL = The Postgres database url."); @@ -633,17 +632,11 @@ async fn windmill_main() -> anyhow::Result<()> { } #[cfg(feature = "operator")] "operator" => { - let sub_arg = std::env::args().nth(2).unwrap_or_default(); - if sub_arg == "crd" { - windmill_operator::print_crd_yaml(); - return Ok(()); - } - tracing_subscriber::fmt::init(); tracing::info!("Starting Windmill Kubernetes operator..."); tracing::info!("Connecting to database..."); let db = crate::db_connect::initial_connection().await?; - tracing::info!("Database connected. Starting controller..."); + tracing::info!("Database connected. Starting ConfigMap watcher..."); windmill_operator::run(db).await?; return Ok(()); } diff --git a/backend/tests/instance_config.rs b/backend/tests/instance_config.rs index 4c85906563..e0ac2a4feb 100644 --- a/backend/tests/instance_config.rs +++ b/backend/tests/instance_config.rs @@ -240,7 +240,7 @@ async fn test_from_db_worker_config_prefix_stripping(db: Pool) { config.worker_configs.contains_key("my_group_name"), "worker__ prefix should be stripped" ); - assert_eq!(config.worker_configs["my_group_name"].cache_clear, Some(5)); + assert_eq!(config.worker_configs["my_group_name"].extra["cache_clear"], serde_json::json!(5)); } #[sqlx::test(fixtures("base"))] @@ -852,7 +852,7 @@ async fn test_full_config_roundtrip(db: Pool) { assert_eq!(otel.tracing_enabled, Some(true)); assert_eq!(config.worker_configs.len(), 2); - assert_eq!(config.worker_configs["default"].cache_clear, Some(7)); + assert_eq!(config.worker_configs["default"].extra["cache_clear"], serde_json::json!(7)); let gpu_auto = config.worker_configs["gpu"].autoscaling.as_ref().unwrap(); assert!(gpu_auto.enabled); assert_eq!(gpu_auto.min_workers, Some(0)); diff --git a/backend/windmill-api-settings/Cargo.toml b/backend/windmill-api-settings/Cargo.toml index 0a79246175..c2efd408b2 100644 --- a/backend/windmill-api-settings/Cargo.toml +++ b/backend/windmill-api-settings/Cargo.toml @@ -28,6 +28,7 @@ lazy_static.workspace = true regex.workspace = true serde.workspace = true serde_json.workspace = true +serde_yml.workspace = true sqlx.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index e2768a8a0f..86233a8d21 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -24,6 +24,8 @@ use windmill_common::usernames::generate_instance_username_for_all_users; use axum::{ extract::{Extension, Path}, routing::{get, post}, + body::Body, + response::Response, Json, Router, }; #[cfg(feature = "enterprise")] @@ -62,6 +64,7 @@ pub fn global_service() -> Router { "/instance_config", get(get_instance_config).put(set_instance_config), ) + .route("/instance_config/yaml", get(get_instance_config_yaml)) .route("/test_smtp", post(test_email)) .route("/test_license_key", post(test_license_key)) .route("/send_stats", post(send_stats)) @@ -409,6 +412,22 @@ async fn get_instance_config( Ok(Json(config)) } +async fn get_instance_config_yaml( + Extension(db): Extension, + authed: ApiAuthed, +) -> error::Result { + require_super_admin(&db, &authed.email).await?; + let config = InstanceConfig::from_db(&db) + .await + .map_err(|e| error::Error::internal_err(e.to_string()))?; + let yaml = serde_yml::to_string(&config) + .map_err(|e| error::Error::internal_err(format!("YAML serialization failed: {e}")))?; + Response::builder() + .header("content-type", "application/yaml") + .body(Body::from(yaml)) + .map_err(|e| error::Error::internal_err(e.to_string())) +} + async fn set_instance_config( Extension(db): Extension, authed: ApiAuthed, @@ -1097,3 +1116,55 @@ async fn sync_cached_resource_types( cached_types.len() - synced_count )) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use windmill_common::instance_config::{GlobalSettings, InstanceConfig, WorkerGroupConfig}; + + #[test] + fn instance_config_yaml_round_trip() { + let config = InstanceConfig { + global_settings: GlobalSettings { + base_url: Some("https://windmill.example.com".to_string()), + retention_period_secs: Some(86400), + expose_metrics: Some(true), + ..Default::default() + }, + worker_configs: BTreeMap::from([( + "default".to_string(), + WorkerGroupConfig { + worker_tags: Some(vec!["deno".to_string(), "python3".to_string()]), + init_bash: Some("apt-get update".to_string()), + ..Default::default() + }, + )]), + }; + + let yaml = serde_yml::to_string(&config).unwrap(); + + // Verify key fields appear in the YAML output + assert!(yaml.contains("base_url: https://windmill.example.com")); + assert!(yaml.contains("retention_period_secs: 86400")); + assert!(yaml.contains("expose_metrics: true")); + assert!(yaml.contains("default:")); + assert!(yaml.contains("- deno")); + assert!(yaml.contains("- python3")); + assert!(yaml.contains("init_bash: apt-get update")); + + // Round-trip back to struct + let deserialized: InstanceConfig = serde_yml::from_str(&yaml).unwrap(); + assert_eq!( + deserialized.global_settings.base_url.as_deref(), + Some("https://windmill.example.com") + ); + assert_eq!(deserialized.global_settings.retention_period_secs, Some(86400)); + assert_eq!(deserialized.global_settings.expose_metrics, Some(true)); + let wc = &deserialized.worker_configs["default"]; + assert_eq!( + wc.worker_tags.as_deref(), + Some(["deno".to_string(), "python3".to_string()].as_slice()) + ); + assert_eq!(wc.init_bash.as_deref(), Some("apt-get update")); + } +} diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index 27c30732b6..91b72d2141 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -701,8 +701,6 @@ pub struct WorkerGroupConfig { #[serde(skip_serializing_if = "Option::is_none")] pub periodic_script_interval_seconds: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub cache_clear: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub additional_python_paths: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub pip_local_dependencies: Option>, @@ -1576,7 +1574,7 @@ mod tests { assert_eq!(config.init_bash.as_deref(), Some("apt-get install -y curl")); assert_eq!(config.periodic_script_bash.as_deref(), Some("echo ping")); assert_eq!(config.periodic_script_interval_seconds, Some(300)); - assert_eq!(config.cache_clear, Some(7)); + assert_eq!(config.extra["cache_clear"], serde_json::json!(7)); assert_eq!(config.additional_python_paths.as_ref().unwrap().len(), 1); assert_eq!(config.pip_local_dependencies.as_ref().unwrap().len(), 1); assert_eq!(config.env_vars_static.as_ref().unwrap()["FOO"], "bar"); diff --git a/backend/windmill-operator/Cargo.toml b/backend/windmill-operator/Cargo.toml index 9ebef0b93b..30a7c0bb7a 100644 --- a/backend/windmill-operator/Cargo.toml +++ b/backend/windmill-operator/Cargo.toml @@ -18,13 +18,11 @@ serde.workspace = true serde_json.workspace = true sqlx.workspace = true tracing.workspace = true -windmill-common = { workspace = true, default-features = false, features = ["instance_config_schema"] } +windmill-common = { workspace = true, default-features = false } anyhow.workspace = true thiserror.workspace = true kube.workspace = true k8s-openapi.workspace = true tokio.workspace = true futures.workspace = true -chrono.workspace = true -schemars = "0.8" serde_yml.workspace = true diff --git a/backend/windmill-operator/manifests/crd.yaml b/backend/windmill-operator/manifests/crd.yaml deleted file mode 100644 index c6093a873b..0000000000 --- a/backend/windmill-operator/manifests/crd.yaml +++ /dev/null @@ -1,703 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: windmillinstances.windmill.dev -spec: - group: windmill.dev - names: - categories: [] - kind: WindmillInstance - plural: windmillinstances - shortNames: - - wmi - singular: windmillinstance - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: '.status.synced' - name: Synced - type: string - - jsonPath: '.status.lastSyncedAt' - name: Last Synced - type: date - - jsonPath: '.metadata.creationTimestamp' - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: Auto-generated derived type for WindmillInstanceSpec via `CustomResource` - properties: - spec: - description: |- - WindmillInstance CRD spec. - - Declares the desired state for instance-level configuration: - `global_settings` maps directly to the `global_settings` table - `worker_configs` maps to the `config` table with a `worker__` prefix - properties: - global_settings: - default: {} - description: Global settings to sync to the `global_settings` table. - properties: - app_workspaced_route: - nullable: true - type: boolean - base_url: - nullable: true - type: string - bunfig_install_scopes: - nullable: true - type: string - critical_alert_mute_ui: - nullable: true - type: boolean - critical_alerts_on_db_oversize: - description: Configuration for critical alerts when the database exceeds a size threshold. - nullable: true - properties: - enabled: - default: false - type: boolean - value: - default: 0.0 - format: float - type: number - type: object - critical_error_channels: - items: - anyOf: - - required: - - email - - required: - - slack_channel - - required: - - teams_channel - description: A channel for delivering critical error alerts. - properties: - email: - type: string - slack_channel: - type: string - teams_channel: - description: Microsoft Teams channel reference. - properties: - channel_id: - type: string - channel_name: - type: string - team_id: - type: string - team_name: - type: string - required: - - channel_id - - channel_name - - team_id - - team_name - type: object - type: object - nullable: true - type: array - custom_instance_pg_databases: - description: Custom PostgreSQL databases managed by the instance. - nullable: true - properties: - databases: - additionalProperties: - description: Status of a single custom instance database. - properties: - error: - nullable: true - type: string - logs: - default: - super_admin: '' - description: Setup log entries for a custom instance database. - properties: - created_database: - type: string - database_credentials: - type: string - db_connect: - type: string - grant_permissions: - type: string - super_admin: - default: '' - type: string - valid_dbname: - type: string - type: object - success: - default: false - type: boolean - tag: - nullable: true - type: string - type: object - type: object - user_pwd: - nullable: true - type: string - type: object - custom_tags: - items: - type: string - nullable: true - type: array - default_tags_per_workspace: - nullable: true - type: boolean - default_tags_workspaces: - items: - type: string - nullable: true - type: array - dev_instance: - nullable: true - type: boolean - disable_stats: - nullable: true - type: boolean - ducklake_settings: - description: DuckLake catalog database settings. - nullable: true - properties: - ducklakes: - additionalProperties: - description: A single DuckLake instance configuration. - properties: - catalog: - description: DuckLake catalog backend reference. - properties: - resource_path: - type: string - resource_type: - description: The type of database backing a DuckLake catalog. - enum: - - postgresql - - mysql - - instance - type: string - required: - - resource_path - - resource_type - type: object - extra_args: - nullable: true - type: string - storage: - description: DuckLake storage location. - properties: - path: - type: string - storage: - nullable: true - type: string - required: - - path - type: object - required: - - catalog - - storage - type: object - type: object - required: - - ducklakes - type: object - email_domain: - nullable: true - type: string - expose_debug_metrics: - nullable: true - type: boolean - expose_metrics: - nullable: true - type: boolean - hub_accessible_url: - nullable: true - type: string - hub_api_secret: - nullable: true - type: string - hub_base_url: - nullable: true - type: string - indexer_settings: - description: Full-text search indexer configuration. - nullable: true - properties: - commit_job_max_batch_size: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - commit_log_max_batch_size: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - max_indexed_job_log_size: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - refresh_index_period: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - refresh_log_index_period: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - should_clear_job_index: - nullable: true - type: boolean - should_clear_log_index: - nullable: true - type: boolean - writer_memory_budget: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - type: object - instance_python_version: - nullable: true - type: string - job_default_timeout: - format: int64 - nullable: true - type: integer - jwt_secret: - nullable: true - type: string - keep_job_dir: - nullable: true - type: boolean - license_key: - nullable: true - type: string - maven_repos: - nullable: true - type: string - min_keep_alive_version: - nullable: true - type: string - monitor_logs_on_s3: - nullable: true - type: boolean - no_default_maven: - nullable: true - type: boolean - npm_config_registry: - nullable: true - type: string - nuget_config: - nullable: true - type: string - oauths: - additionalProperties: - description: OAuth client configuration for a single provider. - properties: - allowed_domains: - items: - type: string - nullable: true - type: array - connect_config: - description: OAuth provider endpoint configuration. - nullable: true - properties: - auth_url: - type: string - extra_params: - additionalProperties: - type: string - nullable: true - type: object - extra_params_callback: - additionalProperties: - type: string - nullable: true - type: object - req_body_auth: - nullable: true - type: boolean - scopes: - items: - type: string - nullable: true - type: array - token_url: - type: string - userinfo_url: - nullable: true - type: string - required: - - auth_url - - token_url - type: object - id: - type: string - login_config: - description: OAuth provider endpoint configuration. - nullable: true - properties: - auth_url: - type: string - extra_params: - additionalProperties: - type: string - nullable: true - type: object - extra_params_callback: - additionalProperties: - type: string - nullable: true - type: object - req_body_auth: - nullable: true - type: boolean - scopes: - items: - type: string - nullable: true - type: array - token_url: - type: string - userinfo_url: - nullable: true - type: string - required: - - auth_url - - token_url - type: object - secret: - type: string - required: - - id - - secret - type: object - nullable: true - type: object - object_store_cache_config: - nullable: true - openai_azure_base_path: - nullable: true - type: string - otel: - description: OpenTelemetry exporter configuration. - nullable: true - properties: - logs_enabled: - nullable: true - type: boolean - metrics_enabled: - nullable: true - type: boolean - otel_exporter_otlp_compression: - nullable: true - type: string - otel_exporter_otlp_endpoint: - nullable: true - type: string - otel_exporter_otlp_headers: - nullable: true - type: string - otel_exporter_otlp_protocol: - nullable: true - type: string - tracing_enabled: - nullable: true - type: boolean - type: object - otel_tracing_proxy: - description: Per-language HTTP request tracing proxy configuration. - nullable: true - properties: - enabled: - default: false - type: boolean - enabled_languages: - items: - description: Script language identifier. - enum: - - python3 - - deno - - go - - bash - - powershell - - postgresql - - bun - - bunnative - - mysql - - bigquery - - snowflake - - graphql - - nativets - - mssql - - oracledb - - duckdb - - php - - rust - - ansible - - csharp - - nu - - java - - ruby - type: string - type: array - type: object - pip_extra_index_url: - nullable: true - type: string - pip_index_url: - nullable: true - type: string - powershell_repo_pat: - nullable: true - type: string - powershell_repo_url: - nullable: true - type: string - request_size_limit_mb: - format: int64 - nullable: true - type: integer - require_preexisting_user_for_oauth: - nullable: true - type: boolean - retention_period_secs: - format: int64 - nullable: true - type: integer - ruby_repos: - nullable: true - type: string - saml_metadata: - nullable: true - type: string - scim_token: - nullable: true - type: string - secret_backend: - nullable: true - slack: - nullable: true - smtp_settings: - description: SMTP server configuration. - nullable: true - properties: - smtp_disable_tls: - nullable: true - type: boolean - smtp_from: - nullable: true - type: string - smtp_host: - nullable: true - type: string - smtp_password: - nullable: true - type: string - smtp_port: - format: uint16 - minimum: 0.0 - nullable: true - type: integer - smtp_tls_implicit: - nullable: true - type: boolean - smtp_username: - nullable: true - type: string - type: object - teams: - nullable: true - timeout_wait_result: - format: int64 - nullable: true - type: integer - type: object - x-kubernetes-preserve-unknown-fields: true - worker_configs: - additionalProperties: - description: Worker group configuration. - properties: - additional_python_paths: - items: - type: string - nullable: true - type: array - autoscaling: - description: Worker group autoscaling configuration. - nullable: true - properties: - cooldown_seconds: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - custom_tags: - items: - type: string - nullable: true - type: array - dec_scale_occupancy_rate: - format: uint8 - minimum: 0.0 - nullable: true - type: integer - enabled: - default: false - type: boolean - full_scale_cooldown_seconds: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - full_scale_jobs_waiting: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - inc_num_workers: - format: uint32 - minimum: 0.0 - nullable: true - type: integer - inc_scale_num_jobs_waiting: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - inc_scale_occupancy_rate: - format: uint8 - minimum: 0.0 - nullable: true - type: integer - integration: - description: |- - Autoscaling integration backend. - - The `type` field selects the backend: `"script"`, `"dryrun"`, or `"kubernetes"`. For `"script"`, `path` is required and `tag` is optional. - nullable: true - properties: - path: - nullable: true - type: string - tag: - nullable: true - type: string - type: - type: string - required: - - type - type: object - max_workers: - format: uint32 - minimum: 0.0 - nullable: true - type: integer - min_workers: - format: uint32 - minimum: 0.0 - nullable: true - type: integer - type: object - cache_clear: - format: uint32 - minimum: 0.0 - nullable: true - type: integer - dedicated_worker: - nullable: true - type: string - dedicated_workers: - items: - type: string - nullable: true - type: array - env_vars_allowlist: - items: - type: string - nullable: true - type: array - env_vars_static: - additionalProperties: - type: string - nullable: true - type: object - init_bash: - nullable: true - type: string - min_alive_workers_alert_threshold: - format: uint32 - minimum: 0.0 - nullable: true - type: integer - periodic_script_bash: - nullable: true - type: string - periodic_script_interval_seconds: - format: uint64 - minimum: 0.0 - nullable: true - type: integer - pip_local_dependencies: - items: - type: string - nullable: true - type: array - priority_tags: - additionalProperties: - format: uint8 - minimum: 0.0 - type: integer - nullable: true - type: object - worker_tags: - items: - type: string - nullable: true - type: array - type: object - x-kubernetes-preserve-unknown-fields: true - default: {} - description: Worker group configs to sync to the `config` table. Keys are worker group names (e.g. "default", "gpu"). Each key is stored in the DB as `worker__`. - type: object - type: object - status: - description: Status subresource for WindmillInstance. - nullable: true - properties: - lastSyncedAt: - description: Timestamp of the last successful sync. - nullable: true - type: string - message: - default: '' - description: Human-readable status message. - type: string - observedGeneration: - default: 0 - description: The `.metadata.generation` that was last observed. - format: int64 - type: integer - synced: - description: Whether the last reconciliation was successful. - type: boolean - required: - - synced - type: object - required: - - spec - title: WindmillInstance - type: object - served: true - storage: true - subresources: - status: {} - diff --git a/backend/windmill-operator/src/lib.rs b/backend/windmill-operator/src/lib.rs index f49335f16d..ffdc4f804c 100644 --- a/backend/windmill-operator/src/lib.rs +++ b/backend/windmill-operator/src/lib.rs @@ -1,5 +1,5 @@ #[cfg(feature = "private")] -pub mod crd_ee; +pub mod configmap_ee; #[cfg(feature = "private")] pub mod db_sync_ee; #[cfg(feature = "private")] diff --git a/backend/windmill-operator/src/operator_oss.rs b/backend/windmill-operator/src/operator_oss.rs index d20cce548a..83cb7635e5 100644 --- a/backend/windmill-operator/src/operator_oss.rs +++ b/backend/windmill-operator/src/operator_oss.rs @@ -1,19 +1,7 @@ #[cfg(feature = "private")] pub use crate::reconciler_ee::run; -#[cfg(feature = "private")] -pub fn print_crd_yaml() { - use kube::CustomResourceExt; - let crd = crate::crd_ee::WindmillInstance::crd(); - println!("{}", serde_yml::to_string(&crd).unwrap()); -} - #[cfg(not(feature = "private"))] pub async fn run(_db: sqlx::Pool) -> anyhow::Result<()> { anyhow::bail!("K8s operator is not available in this build") } - -#[cfg(not(feature = "private"))] -pub fn print_crd_yaml() { - eprintln!("K8s operator CRD generation is not available in this build"); -} diff --git a/examples/deploy/instance-config-iac/README.md b/examples/deploy/instance-config-iac/README.md index a7e8225748..08db7873f0 100644 --- a/examples/deploy/instance-config-iac/README.md +++ b/examples/deploy/instance-config-iac/README.md @@ -173,23 +173,17 @@ If you only want to manage a subset of settings, include all settings you want t ## 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. +The Windmill Kubernetes operator watches a ConfigMap 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 +- A ConfigMap named `windmill-instance` (or a custom name via the `OPERATOR_CONFIGMAP` env var) ### Setup -**1. Install the CRD**: - -```bash -windmill operator crd | kubectl apply -f - -``` - -**2. Create a Kubernetes Secret for sensitive values**: +**1. Create a Kubernetes Secret for sensitive values**: ```yaml apiVersion: v1 @@ -204,78 +198,91 @@ stringData: google-oauth-secret: "your-google-oauth-secret" ``` -**3. Create the WindmillInstance resource** (`windmill-instance.yaml`): +**2. Create the ConfigMap** (`windmill-instance.yaml`): ```yaml -apiVersion: windmill.dev/v1alpha1 -kind: WindmillInstance +apiVersion: v1 +kind: ConfigMap metadata: - name: production + name: windmill-instance 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: +data: + spec: | + global_settings: + base_url: "https://windmill.example.com" + license_key: secretKeyRef: name: windmill-secrets - key: smtp-password - oauths: - google: - id: "google-client-id" - secret: + 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: 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 + 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"] + worker_configs: + default: + worker_tags: ["deno", "python3", "bun", "go", "bash", "powershell"] + init_bash: "echo 'Worker starting'" + native: + worker_tags: ["nativets"] ``` -**4. Apply**: +The config lives under `data.spec` as a YAML string. This is the same schema used by `sync-config`. + +**3. Apply**: ```bash kubectl apply -f windmill-instance.yaml ``` -**5. Check status**: +**4. Check sync status** via events: ```bash -kubectl get wmi -# NAME SYNCED LAST SYNCED AGE -# production true 2025-01-15T10:30:00Z 2d +kubectl get events --field-selector involvedObject.name=windmill-instance ``` +### License key handling + +If `license_key` is absent or empty in the ConfigMap but already exists in the database, the operator preserves the database value. This lets you manage the license key separately (e.g., via the UI) without the operator overwriting it. + +### Environment variables + +| Variable | Default | Description | +|---|---|---| +| `OPERATOR_NAMESPACE` | Pod's own namespace | Namespace of the ConfigMap | +| `OPERATOR_CONFIGMAP` | `windmill-instance` | Name of the ConfigMap to watch | + ### 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 +data: + spec: | + global_settings: + license_key: + envRef: "WM_LICENSE_KEY" # Read from operator pod env ``` The operator pod's Deployment would include: @@ -289,21 +296,22 @@ env: 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.). +This is functionally equivalent to using `secretKeyRef` directly in the ConfigMap, 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: +The operator pod needs permissions to read ConfigMaps, Secrets, and create Events. Minimal Role: ```yaml apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole +kind: Role metadata: name: windmill-operator + namespace: windmill rules: - - apiGroups: ["windmill.dev"] - resources: ["windmillinstances", "windmillinstances/status"] - verbs: ["get", "list", "watch", "patch", "update"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list", "watch"] @@ -312,6 +320,8 @@ rules: verbs: ["create", "patch"] ``` +Note: This is now a namespace-scoped **Role** (not ClusterRole), since there is no CRD to manage. + ### Running the operator ```bash @@ -334,15 +344,3 @@ DATABASE_URL=postgres://... windmill operator | 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. diff --git a/examples/deploy/instance-config-iac/k8s-windmill-instance.yaml b/examples/deploy/instance-config-iac/k8s-windmill-instance.yaml index 977dd86d28..9f8fcf13f0 100644 --- a/examples/deploy/instance-config-iac/k8s-windmill-instance.yaml +++ b/examples/deploy/instance-config-iac/k8s-windmill-instance.yaml @@ -1,67 +1,67 @@ -# Example: WindmillInstance CRD for the Kubernetes operator. +# Example: ConfigMap for the Windmill Kubernetes operator. # # Prerequisites: -# 1. Install the CRD: windmill operator crd | kubectl apply -f - -# 2. Create the Secret: kubectl apply -f k8s-secrets.yaml -# 3. Apply this file: kubectl apply -f k8s-windmill-instance.yaml -# 4. Check status: kubectl get wmi +# 1. Create the Secret: kubectl apply -f k8s-secrets.yaml +# 2. Apply this file: kubectl apply -f k8s-windmill-instance.yaml +# 3. Check events: kubectl get events --field-selector involvedObject.name=windmill-instance -apiVersion: windmill.dev/v1alpha1 -kind: WindmillInstance +apiVersion: v1 +kind: ConfigMap metadata: - name: production + name: windmill-instance namespace: windmill -spec: - global_settings: - base_url: "https://windmill.example.com" +data: + spec: | + global_settings: + base_url: "https://windmill.example.com" - # Secret reference: reads "license-key" from K8s Secret "windmill-secrets" - license_key: - secretKeyRef: - name: windmill-secrets - key: license-key - - retention_period_secs: 2592000 - job_default_timeout: 900 - expose_metrics: true - - smtp_settings: - smtp_host: "smtp.example.com" - smtp_port: 587 - smtp_from: "windmill@example.com" - smtp_tls_implicit: false - smtp_password: + # Secret reference: reads "license-key" from K8s Secret "windmill-secrets" + license_key: secretKeyRef: name: windmill-secrets - key: smtp-password + key: license-key - oauths: - google: - id: "your-google-client-id" - secret: + retention_period_secs: 2592000 + job_default_timeout: 900 + expose_metrics: true + + smtp_settings: + smtp_host: "smtp.example.com" + smtp_port: 587 + smtp_from: "windmill@example.com" + smtp_tls_implicit: false + smtp_password: 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"] + key: smtp-password - custom_tags: - - gpu - - high-mem + oauths: + google: + id: "your-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"] - worker_configs: - default: - worker_tags: - - deno - - python3 - - bun - - go - - bash - - powershell + custom_tags: + - gpu + - high-mem - native: - worker_tags: - - nativets + worker_configs: + default: + worker_tags: + - deno + - python3 + - bun + - go + - bash + - powershell + + native: + worker_tags: + - nativets