mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 08:00:59 +00:00
refactor: switch operator from CRD to ConfigMap (#7972)
* refactor: switch operator from CRD to ConfigMap Replace the WindmillInstance CRD with a plain ConfigMap for the K8s operator. This simplifies deployment (no CRD to install/manage, no ClusterRole for custom API groups) while keeping the same config schema. - Replace crd_ee.rs with configmap_ee.rs (parses data.spec YAML key) - Rewrite reconciler_ee.rs: ConfigMap watcher + Event recorder instead of CRD Controller + status subresource - Add license_key preservation: if absent/empty in ConfigMap but present in DB, the DB value is kept - Remove print_crd_yaml() and "operator crd" subcommand - Drop schemars, chrono, instance_config_schema dependencies - Delete manifests/crd.yaml - Update K8s example and README for ConfigMap approach - RBAC now only needs a namespace-scoped Role (not ClusterRole) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add superadmin YAML export endpoint and remove cache_clear from operator config 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:
Generated
+1
-2
@@ -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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
06207a2083383e0320da287bdf0268454db515de
|
||||
82633d56b2db33ce8fd9ecfda31867fefb4f4823
|
||||
+2
-9
@@ -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 <file> 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 = <required> 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(());
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ async fn test_from_db_worker_config_prefix_stripping(db: Pool<Postgres>) {
|
||||
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<Postgres>) {
|
||||
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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> error::Result<Response> {
|
||||
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<DB>,
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,8 +701,6 @@ pub struct WorkerGroupConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub periodic_script_interval_seconds: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_clear: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub additional_python_paths: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pip_local_dependencies: Option<Vec<String>>,
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__<key>`.
|
||||
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: {}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<sqlx::Postgres>) -> 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");
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user