From 100cfaded46df93cec0060e04de1f6e2d541ed69 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 30 May 2025 15:44:39 +0200 Subject: [PATCH 01/35] Add reconcile function --- Cargo.lock | 15 +- Cargo.nix | 42 +++++- Cargo.toml | 2 + .../opensearch-operator/templates/roles.yaml | 134 ++++++++++++++++++ rust/operator-binary/Cargo.toml | 2 + rust/operator-binary/src/controller.rs | 56 ++++++++ rust/operator-binary/src/main.rs | 81 ++++++++++- 7 files changed, 319 insertions(+), 13 deletions(-) create mode 100644 deploy/helm/opensearch-operator/templates/roles.yaml create mode 100644 rust/operator-binary/src/controller.rs diff --git a/Cargo.lock b/Cargo.lock index 38eb291..64af4cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,6 +753,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.31" @@ -830,6 +836,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures 0.1.31", "futures-channel", "futures-core", "futures-io", @@ -1447,7 +1454,7 @@ dependencies = [ "bytes", "chrono", "either", - "futures", + "futures 0.3.31", "home", "http", "http-body", @@ -1518,7 +1525,7 @@ dependencies = [ "async-stream", "backon", "educe", - "futures", + "futures 0.3.31", "hashbrown 0.15.3", "hostname", "json-patch", @@ -2567,6 +2574,8 @@ version = "0.0.0-dev" dependencies = [ "built", "clap", + "const_format", + "futures 0.3.31", "serde", "serde_json", "snafu 0.8.6", @@ -2588,7 +2597,7 @@ dependencies = [ "dockerfile-parser", "educe", "either", - "futures", + "futures 0.3.31", "indexmap 2.9.0", "json-patch", "k8s-openapi", diff --git a/Cargo.nix b/Cargo.nix index 8fe66a2..8e9b3de 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -2286,7 +2286,20 @@ rec { }; resolvedDefaultFeatures = [ "alloc" "default" "std" ]; }; - "futures" = rec { + "futures 0.1.31" = rec { + crateName = "futures"; + version = "0.1.31"; + edition = "2015"; + sha256 = "0y46qbmhi37dqkch8dlfq5aninqpzqgrr98awkb3rn4fxww1lirs"; + authors = [ + "Alex Crichton " + ]; + features = { + "default" = [ "use_std" "with-deprecated" ]; + }; + resolvedDefaultFeatures = [ "default" "use_std" "with-deprecated" ]; + }; + "futures 0.3.31" = rec { crateName = "futures"; version = "0.3.31"; edition = "2018"; @@ -2345,7 +2358,7 @@ rec { "unstable" = [ "futures-core/unstable" "futures-task/unstable" "futures-channel/unstable" "futures-io/unstable" "futures-util/unstable" ]; "write-all-vectored" = [ "futures-util/write-all-vectored" ]; }; - resolvedDefaultFeatures = [ "alloc" "async-await" "default" "executor" "futures-executor" "std" ]; + resolvedDefaultFeatures = [ "alloc" "async-await" "compat" "default" "executor" "futures-executor" "std" ]; }; "futures-channel" = rec { crateName = "futures-channel"; @@ -2485,6 +2498,12 @@ rec { sha256 = "10aa1ar8bgkgbr4wzxlidkqkcxf77gffyj8j7768h831pcaq784z"; libName = "futures_util"; dependencies = [ + { + name = "futures"; + packageId = "futures 0.1.31"; + rename = "futures_01"; + optional = true; + } { name = "futures-channel"; packageId = "futures-channel"; @@ -2562,7 +2581,7 @@ rec { "unstable" = [ "futures-core/unstable" "futures-task/unstable" ]; "write-all-vectored" = [ "io" ]; }; - resolvedDefaultFeatures = [ "alloc" "async-await" "async-await-macro" "channel" "default" "futures-channel" "futures-io" "futures-macro" "futures-sink" "io" "memchr" "sink" "slab" "std" ]; + resolvedDefaultFeatures = [ "alloc" "async-await" "async-await-macro" "channel" "compat" "default" "futures-channel" "futures-io" "futures-macro" "futures-sink" "futures_01" "io" "memchr" "sink" "slab" "std" ]; }; "generic-array" = rec { crateName = "generic-array"; @@ -4624,7 +4643,7 @@ rec { } { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; optional = true; usesDefaultFeatures = false; features = [ "std" ]; @@ -4759,7 +4778,7 @@ rec { devDependencies = [ { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; usesDefaultFeatures = false; features = [ "async-await" ]; } @@ -4997,7 +5016,7 @@ rec { } { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; usesDefaultFeatures = false; features = [ "async-await" ]; } @@ -8405,6 +8424,15 @@ rec { name = "clap"; packageId = "clap"; } + { + name = "const_format"; + packageId = "const_format"; + } + { + name = "futures"; + packageId = "futures 0.3.31"; + features = [ "compat" ]; + } { name = "serde"; packageId = "serde"; @@ -8496,7 +8524,7 @@ rec { } { name = "futures"; - packageId = "futures"; + packageId = "futures 0.3.31"; } { name = "indexmap"; diff --git a/Cargo.toml b/Cargo.toml index c9b9b02..78861f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", built = { version = "0.8.0", features = ["chrono", "git2"] } clap = "4.5" +const_format = "0.2" +futures = { version = "0.3", features = ["compat"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" snafu = "0.8" diff --git a/deploy/helm/opensearch-operator/templates/roles.yaml b/deploy/helm/opensearch-operator/templates/roles.yaml new file mode 100644 index 0000000..2a1115c --- /dev/null +++ b/deploy/helm/opensearch-operator/templates/roles.yaml @@ -0,0 +1,134 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "operator.fullname" . }}-clusterrole + labels: + {{- include "operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - nodes + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - pods + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - patch + - apiGroups: + - {{ include "operator.name" . }}.stackable.tech + resources: + - {{ include "operator.name" . }}clusters + verbs: + - get + - list + - patch + - watch + - apiGroups: + - {{ include "operator.name" . }}.stackable.tech + resources: + - {{ include "operator.name" . }}clusters/status + verbs: + - patch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - bind + resourceNames: + - {{ include "operator.name" . }}-clusterrole +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "operator.name" . }}-clusterrole + labels: + {{- include "operator.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + - secrets + - serviceaccounts + verbs: + - get + - apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create +{{ if .Capabilities.APIVersions.Has "security.openshift.io/v1" }} + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - nonroot-v2 + verbs: + - use +{{ end }} diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index f8c7c8f..bf5a6cf 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -13,6 +13,8 @@ build = "build.rs" stackable-operator.workspace = true clap.workspace = true +const_format.workspace = true +futures.workspace = true serde.workspace = true serde_json.workspace = true snafu.workspace = true diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs new file mode 100644 index 0000000..f2c08c8 --- /dev/null +++ b/rust/operator-binary/src/controller.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use snafu::Snafu; +use stackable_operator::{ + kube::{ + core::{DeserializeGuard, error_boundary}, + runtime::controller::Action, + }, + logging::controller::ReconcilerError, + time::Duration, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use crate::crd::v1alpha1; + +pub struct Ctx { + pub client: stackable_operator::client::Client, +} + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("OpenSearchCluster object is invalid"))] + InvalidOpenSearchCluster { + source: error_boundary::InvalidObject, + }, +} + +type Result = std::result::Result; + +impl ReconcilerError for Error { + fn category(&self) -> &'static str { + ErrorDiscriminants::from(self).into() + } +} + +pub fn error_policy( + _obj: Arc>, + error: &Error, + _ctx: Arc, +) -> Action { + match error { + // root object is invalid, will be requed when modified + Error::InvalidOpenSearchCluster { .. } => Action::await_change(), + _ => Action::requeue(*Duration::from_secs(5)), + } +} + +pub async fn reconcile( + opensearch: Arc>, + ctx: Arc, +) -> Result { + tracing::info!("Starting reconcile"); + + Ok(Action::await_change()) +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 978c3d0..d26a346 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,14 +1,29 @@ +use std::sync::Arc; + use clap::Parser as _; -use crd::OpenSearchCluster; +use const_format::concatcp; +use crd::{OpenSearchCluster, v1alpha1}; +use futures::StreamExt; use snafu::{ResultExt as _, Snafu}; use stackable_operator::{ YamlSchema as _, cli::{Command, ProductOperatorRun}, + k8s_openapi::api::{apps::v1::StatefulSet, core::v1::Service}, + kube::{ + core::DeserializeGuard, + runtime::{ + Controller, + events::{Recorder, Reporter}, + watcher, + }, + }, + logging::controller::report_controller_reconciled, shared::yaml::SerializeOptions, telemetry::Tracing, }; use strum::{EnumDiscriminants, IntoStaticStr}; +mod controller; mod crd; mod built_info { @@ -35,6 +50,11 @@ pub enum Error { SerializeCrd { source: stackable_operator::shared::yaml::Error, }, + + #[snafu(display("failed to create Kubernetes client"))] + CreateClient { + source: stackable_operator::client::Error, + }, } #[derive(clap::Parser)] @@ -44,6 +64,10 @@ struct Opts { cmd: Command, } +const OPERATOR_NAME: &str = "opensearch.stackable.tech"; +const CONTROLLER_NAME: &str = "opensearchcluster"; +pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); + #[tokio::main] #[snafu::report] async fn main() -> Result<()> { @@ -57,9 +81,9 @@ async fn main() -> Result<()> { } Command::Run(ProductOperatorRun { product_config: _, - watch_namespace: _, + watch_namespace, telemetry_arguments, - cluster_info_opts: _, + cluster_info_opts, }) => { let _tracing_guard = Tracing::pre_configured(built_info::PKG_NAME, telemetry_arguments) .init() @@ -74,6 +98,57 @@ async fn main() -> Result<()> { "Starting {description}", description = built_info::PKG_DESCRIPTION ); + + let client = stackable_operator::client::initialize_operator( + Some(OPERATOR_NAME.to_string()), + &cluster_info_opts, + ) + .await + .context(CreateClientSnafu)?; + + let event_recorder = Arc::new(Recorder::new(client.as_kube_client(), Reporter { + controller: FULL_CONTROLLER_NAME.to_string(), + instance: None, + })); + + let controller = Controller::new( + watch_namespace.get_api::>(&client), + watcher::Config::default(), + ); + controller + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .owns( + watch_namespace.get_api::(&client), + watcher::Config::default(), + ) + .shutdown_on_signal() + .run( + controller::reconcile, + controller::error_policy, + Arc::new(controller::Ctx { + client: client.clone(), + }), + ) + .for_each_concurrent( + 16, // concurrency limit + |result| { + // The event_recorder needs to be shared across all invocations, so that + // events are correctly aggregated + let event_recorder = event_recorder.clone(); + async move { + report_controller_reconciled( + &event_recorder, + FULL_CONTROLLER_NAME, + &result, + ) + .await; + } + }, + ) + .await; } } From 6253fcee00b29dff6fdeefa3a57b298e21d37e7b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 2 Jun 2025 16:12:03 +0200 Subject: [PATCH 02/35] Format Rust code --- rust/operator-binary/src/main.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index d26a346..4c5eacf 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -106,10 +106,13 @@ async fn main() -> Result<()> { .await .context(CreateClientSnafu)?; - let event_recorder = Arc::new(Recorder::new(client.as_kube_client(), Reporter { - controller: FULL_CONTROLLER_NAME.to_string(), - instance: None, - })); + let event_recorder = Arc::new(Recorder::new( + client.as_kube_client(), + Reporter { + controller: FULL_CONTROLLER_NAME.to_string(), + instance: None, + }, + )); let controller = Controller::new( watch_namespace.get_api::>(&client), From fb76424f47f76b588576462f895df9e5b21a0ab8 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 3 Jun 2025 11:47:46 +0200 Subject: [PATCH 03/35] Deploy a StatefulSet --- rust/operator-binary/src/controller.rs | 162 +++++++++++++++++- rust/operator-binary/src/crd/mod.rs | 20 ++- rust/operator-binary/src/main.rs | 4 +- .../kuttl/smoke/10-install-opensearch.yaml | 6 + 4 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 tests/templates/kuttl/smoke/10-install-opensearch.yaml diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index f2c08c8..18152b7 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,17 +1,43 @@ use std::sync::Arc; -use snafu::Snafu; +use const_format::concatcp; +use snafu::{ResultExt, Snafu}; use stackable_operator::{ + builder::{ + meta::ObjectMetaBuilder, + pod::{PodBuilder, container::ContainerBuilder}, + }, + cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, + k8s_openapi::{ + api::{ + apps::v1::{StatefulSet, StatefulSetSpec}, + core::v1::{Container, PodTemplateSpec}, + }, + apimachinery::pkg::apis::meta::v1::LabelSelector, + }, kube::{ + Resource, core::{DeserializeGuard, error_boundary}, runtime::controller::Action, }, + kvp::{Labels, ObjectLabels}, logging::controller::ReconcilerError, + status::condition::{ + compute_conditions, operations::ClusterOperationsConditionBuilder, + statefulset::StatefulSetConditionBuilder, + }, time::Duration, }; use strum::{EnumDiscriminants, IntoStaticStr}; -use crate::crd::v1alpha1; +use crate::{ + OPERATOR_NAME, + crd::v1alpha1::{self, OpenSearchClusterStatus}, +}; + +const CONTROLLER_NAME: &str = "opensearchcluster"; +pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); +const APP_NAME: &str = "opensearch"; pub struct Ctx { pub client: stackable_operator::client::Client, @@ -24,6 +50,21 @@ pub enum Error { InvalidOpenSearchCluster { source: error_boundary::InvalidObject, }, + + #[snafu(display("failed to create cluster resources"))] + CreateClusterResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphanedResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to update status"))] + ApplyStatus { + source: stackable_operator::client::Error, + }, } type Result = std::result::Result; @@ -52,5 +93,122 @@ pub async fn reconcile( ) -> Result { tracing::info!("Starting reconcile"); + let opensearch = opensearch + .0 + .as_ref() + .map_err(error_boundary::InvalidObject::clone) + .context(InvalidOpenSearchClusterSnafu)?; + + let client = &ctx.client; + + let mut cluster_resources = ClusterResources::new( + APP_NAME, + OPERATOR_NAME, + CONTROLLER_NAME, + &opensearch.object_ref(&()), + ClusterResourceApplyStrategy::from(&opensearch.spec.cluster_operation), + ) + .context(CreateClusterResourcesSnafu)?; + + let mut ss_cond_builder = StatefulSetConditionBuilder::default(); + + let statefulset = build_statefulset(opensearch); + + ss_cond_builder.add(cluster_resources.add(client, statefulset).await.unwrap()); + + let cluster_operation_cond_builder = + ClusterOperationsConditionBuilder::new(&opensearch.spec.cluster_operation); + + let status = OpenSearchClusterStatus { + conditions: compute_conditions( + opensearch, + &[&ss_cond_builder, &cluster_operation_cond_builder], + ), + discovery_hash: None, + }; + + cluster_resources + .delete_orphaned_resources(client) + .await + .context(DeleteOrphanedResourcesSnafu)?; + + client + .apply_patch_status(OPERATOR_NAME, opensearch, &status) + .await + .context(ApplyStatusSnafu)?; + Ok(Action::await_change()) } + +fn build_statefulset(opensearch: &v1alpha1::OpenSearchCluster) -> StatefulSet { + let metadata = ObjectMetaBuilder::new() + .name_and_namespace(opensearch) + .with_recommended_labels(ObjectLabels { + owner: opensearch, + app_name: APP_NAME, + app_version: "3.0.0", + operator_name: OPERATOR_NAME, + controller_name: CONTROLLER_NAME, + role: "node", + role_group: "default", + }) + .unwrap() + .build(); + + let template = build_pod_template(opensearch); + + let statefulset_match_labels = + Labels::role_group_selector(opensearch, APP_NAME, "node", "default").unwrap(); + + let spec = StatefulSetSpec { + // Order does not matter for OpenSearch + pod_management_policy: Some("Parallel".to_string()), + replicas: Some(1), + selector: LabelSelector { + match_labels: Some(statefulset_match_labels.into()), + ..LabelSelector::default() + }, + service_name: None, + template, + ..StatefulSetSpec::default() + }; + + StatefulSet { + metadata, + spec: Some(spec), + status: None, + } +} + +fn build_pod_template(opensearch: &v1alpha1::OpenSearchCluster) -> PodTemplateSpec { + let mut builder = PodBuilder::new(); + + let metadata = ObjectMetaBuilder::new() + .with_recommended_labels(ObjectLabels { + owner: opensearch, + app_name: APP_NAME, + app_version: "3.0.0", + operator_name: OPERATOR_NAME, + controller_name: CONTROLLER_NAME, + role: "node", + role_group: "default", + }) + .unwrap() + .build(); + + let container = build_container(); + + builder + .metadata(metadata) + .add_container(container) + .build_template() +} + +fn build_container() -> Container { + ContainerBuilder::new("opensearch") + .expect("ContainerBuilder not created") + .image("opensearchproject/opensearch:3.0.0") + .add_env_var("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "super@Secret1") + .add_env_var("cluster.initial_master_nodes", "opensearch-0") + .build() +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 42cc2c2..91b9a91 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,13 +1,16 @@ use serde::{Deserialize, Serialize}; use stackable_operator::{ + commons::cluster_operation::ClusterOperation, kube::CustomResource, schemars::{self, JsonSchema}, - status::condition::ClusterCondition, + status::condition::{ClusterCondition, HasStatusCondition}, versioned::versioned, }; #[versioned(version(name = "v1alpha1"))] pub mod versioned { + use stackable_operator::commons::cluster_operation::ClusterOperation; + /// A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. /// Find more information on how to use it and the resources that the operator generates in the /// [operator documentation](DOCS_BASE_URL_PLACEHOLDER/opensearch/). @@ -26,7 +29,11 @@ pub mod versioned { ) ))] #[serde(rename_all = "camelCase")] - pub struct OpenSearchClusterSpec {} + pub struct OpenSearchClusterSpec { + // no doc string - See ClusterOperation struct + #[serde(default)] + pub cluster_operation: ClusterOperation, + } #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -39,5 +46,14 @@ pub mod versioned { } } +impl HasStatusCondition for v1alpha1::OpenSearchCluster { + fn conditions(&self) -> Vec { + match &self.status { + Some(status) => status.conditions.clone(), + None => vec![], + } + } +} + #[cfg(test)] mod tests {} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 4c5eacf..badb001 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use clap::Parser as _; -use const_format::concatcp; +use controller::FULL_CONTROLLER_NAME; use crd::{OpenSearchCluster, v1alpha1}; use futures::StreamExt; use snafu::{ResultExt as _, Snafu}; @@ -65,8 +65,6 @@ struct Opts { } const OPERATOR_NAME: &str = "opensearch.stackable.tech"; -const CONTROLLER_NAME: &str = "opensearchcluster"; -pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); #[tokio::main] #[snafu::report] diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml new file mode 100644 index 0000000..60e115b --- /dev/null +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: {} From 641f29dd8fd8d7526c3394b964be180e6b7a9957 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 3 Jun 2025 17:39:50 +0200 Subject: [PATCH 04/35] Structure reconcile function --- rust/operator-binary/src/controller.rs | 100 ++++++++++++++++++++----- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 18152b7..747b5db 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; use const_format::concatcp; use snafu::{ResultExt, Snafu}; @@ -7,11 +7,12 @@ use stackable_operator::{ meta::ObjectMetaBuilder, pod::{PodBuilder, container::ContainerBuilder}, }, + client::Client, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, k8s_openapi::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, - core::v1::{Container, PodTemplateSpec}, + core::v1::{Container, ObjectReference, PodTemplateSpec}, }, apimachinery::pkg::apis::meta::v1::LabelSelector, }, @@ -101,43 +102,108 @@ pub async fn reconcile( let client = &ctx.client; + // resolve + + // validate + + // build + let prepared_resources = build(opensearch); + + // apply + let cluster_ref = opensearch.object_ref(&()); + let apply_strategy = ClusterResourceApplyStrategy::from(&opensearch.spec.cluster_operation); + let applied_resources = apply(client, apply_strategy, &cluster_ref, prepared_resources).await?; + + // update status + update_status(client, opensearch, applied_resources).await?; + + Ok(Action::await_change()) +} + +struct Prepared; +struct Applied; + +struct Resources { + stateful_sets: Vec, + status: PhantomData, +} + +impl Resources { + fn new() -> Self { + Resources { + stateful_sets: vec![], + status: PhantomData, + } + } +} + +fn build(opensearch: &v1alpha1::OpenSearchCluster) -> Resources { + let mut resources = Resources::new(); + + resources.stateful_sets.push(build_statefulset(opensearch)); + + resources +} + +async fn apply( + client: &Client, + apply_strategy: ClusterResourceApplyStrategy, + cluster_ref: &ObjectReference, + resources: Resources, +) -> Result> { let mut cluster_resources = ClusterResources::new( APP_NAME, OPERATOR_NAME, CONTROLLER_NAME, - &opensearch.object_ref(&()), - ClusterResourceApplyStrategy::from(&opensearch.spec.cluster_operation), + cluster_ref, + apply_strategy, ) .context(CreateClusterResourcesSnafu)?; - let mut ss_cond_builder = StatefulSetConditionBuilder::default(); + let mut applied_resources = Resources::new(); + for stateful_set in resources.stateful_sets { + let applied_stateful_set = cluster_resources.add(client, stateful_set).await.unwrap(); + applied_resources.stateful_sets.push(applied_stateful_set); + } + + cluster_resources + .delete_orphaned_resources(client) + .await + .context(DeleteOrphanedResourcesSnafu)?; - let statefulset = build_statefulset(opensearch); + Ok(applied_resources) +} - ss_cond_builder.add(cluster_resources.add(client, statefulset).await.unwrap()); +async fn update_status( + client: &Client, + cluster: &v1alpha1::OpenSearchCluster, + applied_resources: Resources, +) -> Result<()> { + let mut stateful_set_condition_builder = StatefulSetConditionBuilder::default(); + for stateful_set in applied_resources.stateful_sets { + stateful_set_condition_builder.add(stateful_set); + } let cluster_operation_cond_builder = - ClusterOperationsConditionBuilder::new(&opensearch.spec.cluster_operation); + ClusterOperationsConditionBuilder::new(&cluster.spec.cluster_operation); let status = OpenSearchClusterStatus { conditions: compute_conditions( - opensearch, - &[&ss_cond_builder, &cluster_operation_cond_builder], + cluster, + &[ + &stateful_set_condition_builder, + &cluster_operation_cond_builder, + ], ), discovery_hash: None, }; - cluster_resources - .delete_orphaned_resources(client) - .await - .context(DeleteOrphanedResourcesSnafu)?; - client - .apply_patch_status(OPERATOR_NAME, opensearch, &status) + .apply_patch_status(OPERATOR_NAME, cluster, &status) .await .context(ApplyStatusSnafu)?; - Ok(Action::await_change()) + Ok(()) } fn build_statefulset(opensearch: &v1alpha1::OpenSearchCluster) -> StatefulSet { From 370550b02b3459e2af80e5d87c5c0917dc61e664 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 5 Jun 2025 17:01:31 +0200 Subject: [PATCH 05/35] Split controller into validation and build part and introduce some type-safety --- rust/operator-binary/src/controller.rs | 196 ++++++++---------- rust/operator-binary/src/controller/build.rs | 133 ++++++++++++ .../src/controller/validate.rs | 40 ++++ rust/operator-binary/src/crd/mod.rs | 41 +++- rust/operator-binary/src/framework.rs | 69 ++++++ rust/operator-binary/src/framework/kvp.rs | 1 + .../src/framework/kvp/label.rs | 45 ++++ rust/operator-binary/src/main.rs | 5 +- .../kuttl/smoke/10-install-opensearch.yaml | 9 +- 9 files changed, 424 insertions(+), 115 deletions(-) create mode 100644 rust/operator-binary/src/controller/build.rs create mode 100644 rust/operator-binary/src/controller/validate.rs create mode 100644 rust/operator-binary/src/framework.rs create mode 100644 rust/operator-binary/src/framework/kvp.rs create mode 100644 rust/operator-binary/src/framework/kvp/label.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 747b5db..c3ded56 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,28 +1,21 @@ -use std::{marker::PhantomData, sync::Arc}; +use std::{collections::BTreeMap, marker::PhantomData, sync::Arc}; +use build::Builder; use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::{ - meta::ObjectMetaBuilder, - pod::{PodBuilder, container::ContainerBuilder}, - }, client::Client, cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, - k8s_openapi::{ - api::{ - apps::v1::{StatefulSet, StatefulSetSpec}, - core::v1::{Container, ObjectReference, PodTemplateSpec}, - }, - apimachinery::pkg::apis::meta::v1::LabelSelector, - }, + commons::product_image_selection::ProductImage, + k8s_openapi::api::{apps::v1::StatefulSet, core::v1::ObjectReference}, kube::{ Resource, core::{DeserializeGuard, error_boundary}, runtime::controller::Action, }, - kvp::{Labels, ObjectLabels}, + kvp::LabelValueError, logging::controller::ReconcilerError, + role_utils::{GenericProductSpecificCommonConfig, GenericRoleConfig, RoleGroup}, status::condition::{ compute_conditions, operations::ClusterOperationsConditionBuilder, statefulset::StatefulSetConditionBuilder, @@ -30,12 +23,20 @@ use stackable_operator::{ time::Duration, }; use strum::{EnumDiscriminants, IntoStaticStr}; +use validate::validate; use crate::{ OPERATOR_NAME, - crd::v1alpha1::{self, OpenSearchClusterStatus}, + crd::{ + OpenSearchConfigFragment, + v1alpha1::{self, OpenSearchClusterStatus}, + }, + framework::{AppVersion, RoleGroupName, ToLabelValue}, }; +mod build; +mod validate; + const CONTROLLER_NAME: &str = "opensearchcluster"; pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); const APP_NAME: &str = "opensearch"; @@ -66,6 +67,9 @@ pub enum Error { ApplyStatus { source: stackable_operator::client::Error, }, + + #[snafu(display("failed to use as label"))] + InvalidLabelName { source: LabelValueError }, } type Result = std::result::Result; @@ -76,6 +80,66 @@ impl ReconcilerError for Error { } } +type RoleGroupConfig = RoleGroup; + +struct RoleConfig { + role_config: GenericRoleConfig, + role_group_configs: BTreeMap, +} + +// validated and converted to validated and safe types +// no user errors +// not restricted by CRD compliance +pub struct ValidatedCluster { + origin: v1alpha1::OpenSearchCluster, + // cluster: v1alpha1::OpenSearchCluster, + pub image: ProductImage, + pub product_version: AppVersion, + pub name: String, + pub namespace: String, + pub role_config: GenericRoleConfig, + // "validated" means that labels are valid and no ugly rolegroup name broke them + pub role_group_configs: BTreeMap, +} + +impl ToLabelValue for ValidatedCluster { + fn to_label_value(&self) -> String { + // opinionated! + self.origin.to_label_value() + } +} + +// TODO Remove boilerplate +impl Resource for ValidatedCluster { + type DynamicType = + ::DynamicType; + type Scope = ::Scope; + + fn kind(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::kind(dt) + } + + fn group(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::group(dt) + } + + fn version(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::version(dt) + } + + fn plural(dt: &Self::DynamicType) -> std::borrow::Cow<'_, str> { + v1alpha1::OpenSearchCluster::plural(dt) + } + + fn meta(&self) -> &stackable_operator::kube::api::ObjectMeta { + self.origin.meta() + } + + fn meta_mut(&mut self) -> &mut stackable_operator::kube::api::ObjectMeta { + self.origin.meta_mut() + } +} + pub fn error_policy( _obj: Arc>, error: &Error, @@ -89,12 +153,12 @@ pub fn error_policy( } pub async fn reconcile( - opensearch: Arc>, + object: Arc>, ctx: Arc, ) -> Result { tracing::info!("Starting reconcile"); - let opensearch = opensearch + let cluster = object .0 .as_ref() .map_err(error_boundary::InvalidObject::clone) @@ -102,20 +166,21 @@ pub async fn reconcile( let client = &ctx.client; - // resolve + // ~resolve~ dereference (client required) - // validate + // validate (no client required) + let validated_cluster = validate(cluster).unwrap(); - // build - let prepared_resources = build(opensearch); + // build (no client required; infallible) + let prepared_resources = Builder::new(validated_cluster).build(); - // apply - let cluster_ref = opensearch.object_ref(&()); - let apply_strategy = ClusterResourceApplyStrategy::from(&opensearch.spec.cluster_operation); + // apply (client required) + let cluster_ref = cluster.object_ref(&()); + let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); let applied_resources = apply(client, apply_strategy, &cluster_ref, prepared_resources).await?; - // update status - update_status(client, opensearch, applied_resources).await?; + // update status (client required) + update_status(client, cluster, applied_resources).await?; Ok(Action::await_change()) } @@ -137,14 +202,6 @@ impl Resources { } } -fn build(opensearch: &v1alpha1::OpenSearchCluster) -> Resources { - let mut resources = Resources::new(); - - resources.stateful_sets.push(build_statefulset(opensearch)); - - resources -} - async fn apply( client: &Client, apply_strategy: ClusterResourceApplyStrategy, @@ -205,76 +262,3 @@ async fn update_status( Ok(()) } - -fn build_statefulset(opensearch: &v1alpha1::OpenSearchCluster) -> StatefulSet { - let metadata = ObjectMetaBuilder::new() - .name_and_namespace(opensearch) - .with_recommended_labels(ObjectLabels { - owner: opensearch, - app_name: APP_NAME, - app_version: "3.0.0", - operator_name: OPERATOR_NAME, - controller_name: CONTROLLER_NAME, - role: "node", - role_group: "default", - }) - .unwrap() - .build(); - - let template = build_pod_template(opensearch); - - let statefulset_match_labels = - Labels::role_group_selector(opensearch, APP_NAME, "node", "default").unwrap(); - - let spec = StatefulSetSpec { - // Order does not matter for OpenSearch - pod_management_policy: Some("Parallel".to_string()), - replicas: Some(1), - selector: LabelSelector { - match_labels: Some(statefulset_match_labels.into()), - ..LabelSelector::default() - }, - service_name: None, - template, - ..StatefulSetSpec::default() - }; - - StatefulSet { - metadata, - spec: Some(spec), - status: None, - } -} - -fn build_pod_template(opensearch: &v1alpha1::OpenSearchCluster) -> PodTemplateSpec { - let mut builder = PodBuilder::new(); - - let metadata = ObjectMetaBuilder::new() - .with_recommended_labels(ObjectLabels { - owner: opensearch, - app_name: APP_NAME, - app_version: "3.0.0", - operator_name: OPERATOR_NAME, - controller_name: CONTROLLER_NAME, - role: "node", - role_group: "default", - }) - .unwrap() - .build(); - - let container = build_container(); - - builder - .metadata(metadata) - .add_container(container) - .build_template() -} - -fn build_container() -> Container { - ContainerBuilder::new("opensearch") - .expect("ContainerBuilder not created") - .image("opensearchproject/opensearch:3.0.0") - .add_env_var("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "super@Secret1") - .add_env_var("cluster.initial_master_nodes", "opensearch-0") - .build() -} diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs new file mode 100644 index 0000000..42de350 --- /dev/null +++ b/rust/operator-binary/src/controller/build.rs @@ -0,0 +1,133 @@ +use std::str::FromStr; + +use stackable_operator::{ + builder::{ + meta::ObjectMetaBuilder, + pod::{PodBuilder, container::ContainerBuilder}, + }, + k8s_openapi::{ + api::{ + apps::v1::{StatefulSet, StatefulSetSpec}, + core::v1::{Container, PodTemplateSpec}, + }, + apimachinery::pkg::apis::meta::v1::LabelSelector, + }, + kvp::Labels, +}; + +use super::{APP_NAME, CONTROLLER_NAME, Prepared, Resources, RoleGroupName, ValidatedCluster}; +use crate::{ + OPERATOR_NAME, + framework::{ + AppName, ControllerName, OperatorName, RoleName, + kvp::label::{recommended_labels, role_group_selector}, + }, +}; + +pub struct Builder { + app_name: AppName, + operator_name: OperatorName, + controller_name: ControllerName, + role_name: RoleName, + cluster: ValidatedCluster, +} + +impl Builder { + pub fn new(cluster: ValidatedCluster) -> Builder { + Builder { + app_name: AppName::from_str(APP_NAME).unwrap(), + role_name: RoleName::from_str("nodes").unwrap(), + operator_name: OperatorName::from_str(OPERATOR_NAME).unwrap(), + controller_name: ControllerName::from_str(CONTROLLER_NAME).unwrap(), + cluster, + } + } + + pub fn build(&self) -> Resources { + let mut resources = Resources::new(); + for role_group_name in self.cluster.role_group_configs.keys() { + resources + .stateful_sets + .push(self.build_statefulset(role_group_name)); + } + resources + } + + fn build_statefulset(&self, role_group_name: &RoleGroupName) -> StatefulSet { + let metadata = ObjectMetaBuilder::new() + .name(&self.cluster.name) + .namespace(&self.cluster.namespace) + .with_labels(self.build_recommended_labels(role_group_name)) + .build(); + + let template = self.build_pod_template(role_group_name); + + let statefulset_match_labels = role_group_selector( + &self.cluster, + &self.app_name, + &self.role_name, + role_group_name, + ); + + let spec = StatefulSetSpec { + // Order does not matter for OpenSearch + pod_management_policy: Some("Parallel".to_string()), + replicas: Some(1), + selector: LabelSelector { + match_labels: Some(statefulset_match_labels.into()), + ..LabelSelector::default() + }, + service_name: None, + template, + ..StatefulSetSpec::default() + }; + + StatefulSet { + metadata, + spec: Some(spec), + status: None, + } + } + + fn build_pod_template(&self, role_group_name: &RoleGroupName) -> PodTemplateSpec { + let mut builder = PodBuilder::new(); + + let metadata = ObjectMetaBuilder::new() + .with_labels(self.build_recommended_labels(role_group_name)) + .build(); + + let container = self.build_container(); + + builder + .metadata(metadata) + .add_container(container) + .build_template() + } + + fn build_container(&self) -> Container { + let product_image = self + .cluster + .image + .resolve("opensearch", crate::built_info::PKG_VERSION); + + // TODO ContainerName as typed string? + ContainerBuilder::new("opensearch") + .expect("ContainerBuilder not created") + .image_from_product_image(&product_image) + .add_env_var("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "super@Secret1") + .add_env_var("cluster.initial_master_nodes", "opensearch-0") + .build() + } + + fn build_recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { + recommended_labels( + &self.cluster, + &self.app_name, + &self.cluster.product_version, + &self.operator_name, + &self.controller_name, + &self.role_name, + role_group_name, + ) + } +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs new file mode 100644 index 0000000..dbb2dc5 --- /dev/null +++ b/rust/operator-binary/src/controller/validate.rs @@ -0,0 +1,40 @@ +use std::{collections::BTreeMap, str::FromStr}; + +use snafu::Snafu; +use stackable_operator::{kube::ResourceExt, kvp::LabelError}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use super::{AppVersion, RoleGroupName, ValidatedCluster}; +use crate::crd::v1alpha1; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to set recommended labels"))] + RecommendedLabels { source: LabelError }, +} + +type Result = std::result::Result; + +// no client needed +pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result { + let product_version = AppVersion::from_str(cluster.spec.image.product_version()).expect("oops"); + + let mut role_group_configs = BTreeMap::new(); + for (raw_role_group_name, role_group_config) in &cluster.spec.nodes.role_groups { + let role_group_name = RoleGroupName::from_str(raw_role_group_name).unwrap(); + role_group_configs.insert(role_group_name, role_group_config.clone()); + + // TODO merge configs + } + + Ok(ValidatedCluster { + origin: cluster.to_owned(), + image: cluster.spec.image.clone(), + product_version, + name: cluster.name_unchecked(), + namespace: cluster.namespace().expect("muss da sein"), + role_config: cluster.spec.nodes.role_config.clone(), + role_group_configs, + }) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 91b9a91..1d1b006 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,16 +1,18 @@ use serde::{Deserialize, Serialize}; use stackable_operator::{ - commons::cluster_operation::ClusterOperation, - kube::CustomResource, + commons::{cluster_operation::ClusterOperation, product_image_selection::ProductImage}, + config::{fragment::Fragment, merge::Merge}, + kube::{CustomResource, ResourceExt}, + role_utils::Role, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, versioned::versioned, }; +use crate::framework::ToLabelValue; + #[versioned(version(name = "v1alpha1"))] pub mod versioned { - use stackable_operator::commons::cluster_operation::ClusterOperation; - /// A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. /// Find more information on how to use it and the resources that the operator generates in the /// [operator documentation](DOCS_BASE_URL_PLACEHOLDER/opensearch/). @@ -30,9 +32,15 @@ pub mod versioned { ))] #[serde(rename_all = "camelCase")] pub struct OpenSearchClusterSpec { + // no doc string - See ProductImage struct + pub image: ProductImage, + // no doc string - See ClusterOperation struct #[serde(default)] pub cluster_operation: ClusterOperation, + + // Only one role here! + pub nodes: Role, } #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -55,5 +63,26 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { } } -#[cfg(test)] -mod tests {} +impl ToLabelValue for v1alpha1::OpenSearchCluster { + fn to_label_value(&self) -> String { + // opinionated! + self.name_unchecked() + } +} + +// TODO Perhaps rename to InstanceConfig +#[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] +#[fragment_attrs( + derive( + Clone, + Debug, + Default, + Deserialize, + Merge, + JsonSchema, + PartialEq, + Serialize + ), + serde(rename_all = "camelCase") +)] +pub struct OpenSearchConfig {} diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs new file mode 100644 index 0000000..3106bfe --- /dev/null +++ b/rust/operator-binary/src/framework.rs @@ -0,0 +1,69 @@ +// Type-safe wrappers that cannot throw errors +// The point is, to move the validation "upwards". + +use std::str::FromStr; + +use snafu::{ResultExt, Snafu}; +use stackable_operator::kvp::{LabelValue, LabelValueError}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +pub mod kvp; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to use as label"))] + InvalidLabelName { source: LabelValueError }, +} + +pub trait ToLabelValue { + fn to_label_value(&self) -> String; +} + +macro_rules! typed_string { + ($name:ident) => { + #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] + pub struct $name(String); + + impl ToLabelValue for $name { + fn to_label_value(&self) -> String { + self.0.clone() + } + } + + impl FromStr for $name { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + LabelValue::from_str(s).context(InvalidLabelNameSnafu)?; + Ok(Self(s.to_owned())) + } + } + }; +} + +typed_string!(AppName); +typed_string!(AppVersion); +typed_string!(ControllerName); +typed_string!(OperatorName); +typed_string!(RoleGroupName); +typed_string!(RoleName); + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::framework::AppName; + + #[test] + fn test_typed_string_constraints() { + assert!(AppName::from_str("valid-role-group-name").is_ok()); + assert!(AppName::from_str("invalid-character: /").is_err()); + assert!( + AppName::from_str( + "too-long-123456789012345678901234567890123456789012345678901234567890" + ) + .is_err() + ); + } +} diff --git a/rust/operator-binary/src/framework/kvp.rs b/rust/operator-binary/src/framework/kvp.rs new file mode 100644 index 0000000..0006163 --- /dev/null +++ b/rust/operator-binary/src/framework/kvp.rs @@ -0,0 +1 @@ +pub mod label; diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs new file mode 100644 index 0000000..fe8e413 --- /dev/null +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -0,0 +1,45 @@ +use stackable_operator::{ + kube::Resource, + kvp::{Labels, ObjectLabels}, +}; + +use crate::framework::{ + AppName, AppVersion, ControllerName, OperatorName, RoleGroupName, RoleName, ToLabelValue, +}; + +pub fn recommended_labels( + owner: &(impl Resource + ToLabelValue), + app_name: &AppName, + app_version: &AppVersion, + operator_name: &OperatorName, + controller_name: &ControllerName, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + let object_labels = ObjectLabels { + owner, + app_name: &app_name.to_label_value(), + app_version: &app_version.to_label_value(), + operator_name: &operator_name.to_label_value(), + controller_name: &controller_name.to_label_value(), + role: &role_name.to_label_value(), + role_group: &role_group_name.to_label_value(), + }; + Labels::recommended(object_labels) + .expect("Labels should be created because all given parameters produce valid label values") +} + +pub fn role_group_selector( + owner: &(impl Resource + ToLabelValue), + app_name: &AppName, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> Labels { + Labels::role_group_selector( + owner, + &app_name.to_label_value(), + &role_name.to_label_value(), + &role_group_name.to_label_value(), + ) + .expect("Labels should be created because all given parameters produce valid label values") +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index badb001..871aed7 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -25,6 +25,7 @@ use strum::{EnumDiscriminants, IntoStaticStr}; mod controller; mod crd; +mod framework; mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); @@ -98,7 +99,7 @@ async fn main() -> Result<()> { ); let client = stackable_operator::client::initialize_operator( - Some(OPERATOR_NAME.to_string()), + Some(OPERATOR_NAME.to_owned()), &cluster_info_opts, ) .await @@ -107,7 +108,7 @@ async fn main() -> Result<()> { let event_recorder = Arc::new(Recorder::new( client.as_kube_client(), Reporter { - controller: FULL_CONTROLLER_NAME.to_string(), + controller: FULL_CONTROLLER_NAME.to_owned(), instance: None, }, )); diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 60e115b..3aed8a0 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -3,4 +3,11 @@ apiVersion: opensearch.stackable.tech/v1alpha1 kind: OpenSearchCluster metadata: name: opensearch -spec: {} +spec: + image: + custom: opensearchproject/opensearch:3.0.0 + productVersion: 3.0.0 + nodes: + roleGroups: + default: + replicas: 1 From 922a5097229cabe0b51824d5abd84cb0299de0dd Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 6 Jun 2025 11:06:39 +0200 Subject: [PATCH 06/35] Use the qualified role group name for the StatefulSet --- rust/operator-binary/src/controller.rs | 13 +- rust/operator-binary/src/controller/build.rs | 25 +++- .../src/controller/validate.rs | 18 ++- rust/operator-binary/src/framework.rs | 120 +++++++++++++++--- 4 files changed, 138 insertions(+), 38 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index c3ded56..581a460 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -31,7 +31,7 @@ use crate::{ OpenSearchConfigFragment, v1alpha1::{self, OpenSearchClusterStatus}, }, - framework::{AppVersion, RoleGroupName, ToLabelValue}, + framework::{AppVersion, ClusterName, RoleGroupName, ToLabelValue}, }; mod build; @@ -82,20 +82,14 @@ impl ReconcilerError for Error { type RoleGroupConfig = RoleGroup; -struct RoleConfig { - role_config: GenericRoleConfig, - role_group_configs: BTreeMap, -} - // validated and converted to validated and safe types // no user errors // not restricted by CRD compliance pub struct ValidatedCluster { origin: v1alpha1::OpenSearchCluster, - // cluster: v1alpha1::OpenSearchCluster, pub image: ProductImage, pub product_version: AppVersion, - pub name: String, + pub name: ClusterName, pub namespace: String, pub role_config: GenericRoleConfig, // "validated" means that labels are valid and no ugly rolegroup name broke them @@ -109,7 +103,7 @@ impl ToLabelValue for ValidatedCluster { } } -// TODO Remove boilerplate +// TODO Remove boilerplate (like derive_more) impl Resource for ValidatedCluster { type DynamicType = ::DynamicType; @@ -185,6 +179,7 @@ pub async fn reconcile( Ok(Action::await_change()) } +// Marker struct Prepared; struct Applied; diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 42de350..d802e6d 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -15,12 +15,16 @@ use stackable_operator::{ kvp::Labels, }; -use super::{APP_NAME, CONTROLLER_NAME, Prepared, Resources, RoleGroupName, ValidatedCluster}; +use super::{ + APP_NAME, CONTROLLER_NAME, Prepared, Resources, RoleGroupConfig, RoleGroupName, + ValidatedCluster, +}; use crate::{ OPERATOR_NAME, framework::{ - AppName, ControllerName, OperatorName, RoleName, + AppName, ControllerName, OperatorName, RoleName, ToObjectName, kvp::label::{recommended_labels, role_group_selector}, + qualified_role_group_name, }, }; @@ -45,17 +49,24 @@ impl Builder { pub fn build(&self) -> Resources { let mut resources = Resources::new(); - for role_group_name in self.cluster.role_group_configs.keys() { + for (role_group_name, role_group_config) in self.cluster.role_group_configs.iter() { resources .stateful_sets - .push(self.build_statefulset(role_group_name)); + .push(self.build_statefulset(role_group_name, role_group_config)); } resources } - fn build_statefulset(&self, role_group_name: &RoleGroupName) -> StatefulSet { + fn build_statefulset( + &self, + role_group_name: &RoleGroupName, + role_group_config: &RoleGroupConfig, + ) -> StatefulSet { let metadata = ObjectMetaBuilder::new() - .name(&self.cluster.name) + .name( + qualified_role_group_name(&self.cluster.name, &self.role_name, role_group_name) + .to_object_name(), + ) .namespace(&self.cluster.namespace) .with_labels(self.build_recommended_labels(role_group_name)) .build(); @@ -72,7 +83,7 @@ impl Builder { let spec = StatefulSetSpec { // Order does not matter for OpenSearch pod_management_policy: Some("Parallel".to_string()), - replicas: Some(1), + replicas: role_group_config.replicas.map(i32::from), selector: LabelSelector { match_labels: Some(statefulset_match_labels.into()), ..LabelSelector::default() diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index dbb2dc5..01f570e 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,23 +1,31 @@ use std::{collections::BTreeMap, str::FromStr}; -use snafu::Snafu; -use stackable_operator::{kube::ResourceExt, kvp::LabelError}; +use snafu::{ResultExt, Snafu}; +use stackable_operator::kube::ResourceExt; use strum::{EnumDiscriminants, IntoStaticStr}; use super::{AppVersion, RoleGroupName, ValidatedCluster}; -use crate::crd::v1alpha1; +use crate::{crd::v1alpha1, framework::ClusterName}; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { #[snafu(display("failed to set recommended labels"))] - RecommendedLabels { source: LabelError }, + InvalidClusterName { source: crate::framework::Error }, + + #[snafu(display("failed to set recommended labels"))] + RecommendedLabels { + source: stackable_operator::kvp::LabelError, + }, } type Result = std::result::Result; // no client needed pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result { + let cluster_name = + ClusterName::from_str(&cluster.name_unchecked()).context(InvalidClusterNameSnafu)?; + let product_version = AppVersion::from_str(cluster.spec.image.product_version()).expect("oops"); let mut role_group_configs = BTreeMap::new(); @@ -32,7 +40,7 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result String; } pub trait ToLabelValue { fn to_label_value(&self) -> String; } -macro_rules! typed_string { - ($name:ident) => { +macro_rules! object_name { + ($name:ident, $max_length:literal) => { + /// Bla #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] - pub struct $name(String); + pub struct $name { + value: String, + // could be somehow static + // type arithmetic would be better + max_length: u32, + } + + impl Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } + } + + impl ToObjectName for $name { + fn to_object_name(&self) -> String { + self.value.clone() + } + } impl ToLabelValue for $name { fn to_label_value(&self) -> String { - self.0.clone() + self.value.clone() } } @@ -35,25 +67,65 @@ macro_rules! typed_string { type Err = Error; fn from_str(s: &str) -> std::result::Result { - LabelValue::from_str(s).context(InvalidLabelNameSnafu)?; - Ok(Self(s.to_owned())) + let length = s.len() as u32; + ensure!( + length < $max_length, + LengthExceededSnafu { + length, + max_length: $max_length + } + ); + + stackable_operator::validation::is_rfc_1123_subdomain(s) + .context(InvalidObjectNameSnafu)?; + + LabelValue::from_str(s).context(InvalidLabelValueSnafu)?; + + Ok(Self { + value: s.to_owned(), + max_length: $max_length, + }) } } }; } -typed_string!(AppName); -typed_string!(AppVersion); -typed_string!(ControllerName); -typed_string!(OperatorName); -typed_string!(RoleGroupName); -typed_string!(RoleName); +object_name!(AppName, 63u32); +object_name!(AppVersion, 63u32); +object_name!(ClusterName, 63u32); +object_name!(ControllerName, 63u32); +object_name!(OperatorName, 63u32); +object_name!(RoleGroupName, 63u32); +object_name!(RoleName, 63u32); +object_name!(QualifiedRoleGroupName, 250u32); + +pub fn qualified_role_group_name( + cluster_name: &ClusterName, + role_name: &RoleName, + role_group_name: &RoleGroupName, +) -> QualifiedRoleGroupName { + // This assertion is already checked when running the unit test below, so it is not expected to + // fail at runtime of the operator. + assert!( + cluster_name.max_length + role_name.max_length + role_group_name.max_length < 250, + "The maximum lengths of the cluster name, role name and role group name must be defined so that the combination of these names (including separators and the sequential pod number) is also a valid object name with a maximum of 263 characters (see RFC 1123)" + ); + + QualifiedRoleGroupName::from_str(&format!( + "{}-{}-{}", + cluster_name.to_object_name(), + role_name.to_object_name(), + role_group_name.to_object_name() + )) + .expect("") +} #[cfg(test)] mod tests { use std::str::FromStr; - use crate::framework::AppName; + use super::{ClusterName, RoleGroupName, RoleName, qualified_role_group_name}; + use crate::framework::{AppName, ToObjectName}; #[test] fn test_typed_string_constraints() { @@ -66,4 +138,18 @@ mod tests { .is_err() ); } + + #[test] + fn test_qualified_role_group_name() { + let qualified_role_group_name = qualified_role_group_name( + &ClusterName::from_str("test-cluster").expect("should be a valid cluster name"), + &RoleName::from_str("data-nodes").expect("should be a valid role name"), + &RoleGroupName::from_str("ssd-storage").expect("should be a valid role group name"), + ); + + assert_eq!( + "test-cluster-data-nodes-ssd-storage", + qualified_role_group_name.to_object_name() + ); + } } From d6d9b0d13474e346245552486fcc90304d812a58 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 6 Jun 2025 18:14:30 +0200 Subject: [PATCH 07/35] Refactor "apply" and "update_status" --- rust/operator-binary/src/controller.rs | 163 +++++++----------- rust/operator-binary/src/controller/apply.rs | 58 +++++++ rust/operator-binary/src/controller/build.rs | 14 +- .../src/controller/update_status.rs | 68 ++++++++ rust/operator-binary/src/crd/mod.rs | 11 +- rust/operator-binary/src/framework.rs | 99 ++++++----- .../src/framework/cluster_resources.rs | 39 +++++ .../src/framework/kvp/label.rs | 8 +- 8 files changed, 298 insertions(+), 162 deletions(-) create mode 100644 rust/operator-binary/src/controller/apply.rs create mode 100644 rust/operator-binary/src/controller/update_status.rs create mode 100644 rust/operator-binary/src/framework/cluster_resources.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 581a460..93e8c0f 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,40 +1,37 @@ -use std::{collections::BTreeMap, marker::PhantomData, sync::Arc}; +use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc}; +use apply::apply; use build::Builder; use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ - client::Client, - cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, + cluster_resources::ClusterResourceApplyStrategy, commons::product_image_selection::ProductImage, - k8s_openapi::api::{apps::v1::StatefulSet, core::v1::ObjectReference}, - kube::{ - Resource, - core::{DeserializeGuard, error_boundary}, - runtime::controller::Action, - }, - kvp::LabelValueError, + k8s_openapi::api::apps::v1::StatefulSet, + kube::{Resource, core::DeserializeGuard, runtime::controller::Action}, logging::controller::ReconcilerError, role_utils::{GenericProductSpecificCommonConfig, GenericRoleConfig, RoleGroup}, - status::condition::{ - compute_conditions, operations::ClusterOperationsConditionBuilder, - statefulset::StatefulSetConditionBuilder, - }, time::Duration, }; use strum::{EnumDiscriminants, IntoStaticStr}; +use update_status::update_status; use validate::validate; use crate::{ OPERATOR_NAME, crd::{ OpenSearchConfigFragment, - v1alpha1::{self, OpenSearchClusterStatus}, + v1alpha1::{self}, + }, + framework::{ + AppName, AppVersion, ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, + IsLabelValue, OperatorName, RoleGroupName, RoleName, }, - framework::{AppVersion, ClusterName, RoleGroupName, ToLabelValue}, }; +mod apply; mod build; +mod update_status; mod validate; const CONTROLLER_NAME: &str = "opensearchcluster"; @@ -50,26 +47,15 @@ pub struct Ctx { pub enum Error { #[snafu(display("OpenSearchCluster object is invalid"))] InvalidOpenSearchCluster { - source: error_boundary::InvalidObject, + // boxed because otherwise Clippy warns about a large enum variant + source: Box, }, - #[snafu(display("failed to create cluster resources"))] - CreateClusterResources { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to delete orphaned resources"))] - DeleteOrphanedResources { - source: stackable_operator::cluster_resources::Error, - }, + #[snafu(display("failed to apply resources"))] + ApplyResources { source: apply::Error }, #[snafu(display("failed to update status"))] - ApplyStatus { - source: stackable_operator::client::Error, - }, - - #[snafu(display("failed to use as label"))] - InvalidLabelName { source: LabelValueError }, + UpdateStatus { source: update_status::Error }, } type Result = std::result::Result; @@ -85,6 +71,7 @@ type RoleGroupConfig = RoleGroup, } -impl ToLabelValue for ValidatedCluster { +impl HasObjectName for ValidatedCluster { + fn to_object_name(&self) -> String { + self.name.to_object_name() + } +} + +impl HasNamespace for ValidatedCluster { + fn to_namespace(&self) -> String { + self.namespace.clone() + } +} + +impl HasUid for ValidatedCluster { + fn to_uid(&self) -> String { + // TODO fix + self.origin.metadata.uid.clone().unwrap() + } +} + +// ? +impl IsLabelValue for ValidatedCluster { fn to_label_value(&self) -> String { // opinionated! - self.origin.to_label_value() + self.name.to_label_value() } } @@ -155,7 +162,8 @@ pub async fn reconcile( let cluster = object .0 .as_ref() - .map_err(error_boundary::InvalidObject::clone) + .map_err(stackable_operator::kube::core::error_boundary::InvalidObject::clone) + .map_err(Box::new) .context(InvalidOpenSearchClusterSnafu)?; let client = &ctx.client; @@ -166,15 +174,31 @@ pub async fn reconcile( let validated_cluster = validate(cluster).unwrap(); // build (no client required; infallible) - let prepared_resources = Builder::new(validated_cluster).build(); + let prepared_resources = Builder::new(validated_cluster.clone()).build(); // apply (client required) - let cluster_ref = cluster.object_ref(&()); + // + // into controller context! + let app_name = AppName::from_str(APP_NAME).unwrap(); + let operator_name = OperatorName::from_str(OPERATOR_NAME).unwrap(); + let controller_name = ControllerName::from_str(CONTROLLER_NAME).unwrap(); let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); - let applied_resources = apply(client, apply_strategy, &cluster_ref, prepared_resources).await?; + let applied_resources = apply( + client, + &app_name, + &operator_name, + &controller_name, + &validated_cluster, + apply_strategy, + prepared_resources, + ) + .await + .context(ApplyResourcesSnafu)?; // update status (client required) - update_status(client, cluster, applied_resources).await?; + update_status(client, cluster, applied_resources) + .await + .context(UpdateStatusSnafu)?; Ok(Action::await_change()) } @@ -196,64 +220,3 @@ impl Resources { } } } - -async fn apply( - client: &Client, - apply_strategy: ClusterResourceApplyStrategy, - cluster_ref: &ObjectReference, - resources: Resources, -) -> Result> { - let mut cluster_resources = ClusterResources::new( - APP_NAME, - OPERATOR_NAME, - CONTROLLER_NAME, - cluster_ref, - apply_strategy, - ) - .context(CreateClusterResourcesSnafu)?; - - let mut applied_resources = Resources::new(); - for stateful_set in resources.stateful_sets { - let applied_stateful_set = cluster_resources.add(client, stateful_set).await.unwrap(); - applied_resources.stateful_sets.push(applied_stateful_set); - } - - cluster_resources - .delete_orphaned_resources(client) - .await - .context(DeleteOrphanedResourcesSnafu)?; - - Ok(applied_resources) -} - -async fn update_status( - client: &Client, - cluster: &v1alpha1::OpenSearchCluster, - applied_resources: Resources, -) -> Result<()> { - let mut stateful_set_condition_builder = StatefulSetConditionBuilder::default(); - for stateful_set in applied_resources.stateful_sets { - stateful_set_condition_builder.add(stateful_set); - } - - let cluster_operation_cond_builder = - ClusterOperationsConditionBuilder::new(&cluster.spec.cluster_operation); - - let status = OpenSearchClusterStatus { - conditions: compute_conditions( - cluster, - &[ - &stateful_set_condition_builder, - &cluster_operation_cond_builder, - ], - ), - discovery_hash: None, - }; - - client - .apply_patch_status(OPERATOR_NAME, cluster, &status) - .await - .context(ApplyStatusSnafu)?; - - Ok(()) -} diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs new file mode 100644 index 0000000..0104c49 --- /dev/null +++ b/rust/operator-binary/src/controller/apply.rs @@ -0,0 +1,58 @@ +use snafu::{ResultExt, Snafu}; +use stackable_operator::{client::Client, cluster_resources::ClusterResourceApplyStrategy}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use super::{Applied, Prepared, Resources}; +use crate::framework::{ + AppName, ControllerName, HasNamespace, HasObjectName, HasUid, OperatorName, + cluster_resources::cluster_resources_new, +}; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphanedResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to update status"))] + ApplyStatus { + source: stackable_operator::client::Error, + }, +} + +type Result = std::result::Result; + +pub async fn apply( + client: &Client, + // app_name, operator_name, ... in context? + app_name: &AppName, + operator_name: &OperatorName, + controller_name: &ControllerName, + cluster: &(impl HasObjectName + HasNamespace + HasUid), + // ref? + apply_strategy: ClusterResourceApplyStrategy, + resources: Resources, +) -> Result> { + let mut cluster_resources = cluster_resources_new( + app_name, + operator_name, + controller_name, + cluster, + apply_strategy, + ); + + let mut applied_resources = Resources::new(); + for stateful_set in resources.stateful_sets { + let applied_stateful_set = cluster_resources.add(client, stateful_set).await.unwrap(); + applied_resources.stateful_sets.push(applied_stateful_set); + } + + cluster_resources + .delete_orphaned_resources(client) + .await + .context(DeleteOrphanedResourcesSnafu)?; + + Ok(applied_resources) +} diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index d802e6d..7608024 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -22,9 +22,9 @@ use super::{ use crate::{ OPERATOR_NAME, framework::{ - AppName, ControllerName, OperatorName, RoleName, ToObjectName, + AppName, ControllerName, OperatorName, RoleName, kvp::label::{recommended_labels, role_group_selector}, - qualified_role_group_name, + to_qualified_role_group_name, }, }; @@ -39,6 +39,7 @@ pub struct Builder { impl Builder { pub fn new(cluster: ValidatedCluster) -> Builder { Builder { + // into controller context! app_name: AppName::from_str(APP_NAME).unwrap(), role_name: RoleName::from_str("nodes").unwrap(), operator_name: OperatorName::from_str(OPERATOR_NAME).unwrap(), @@ -63,10 +64,11 @@ impl Builder { role_group_config: &RoleGroupConfig, ) -> StatefulSet { let metadata = ObjectMetaBuilder::new() - .name( - qualified_role_group_name(&self.cluster.name, &self.role_name, role_group_name) - .to_object_name(), - ) + .name(to_qualified_role_group_name( + &self.cluster.name, + &self.role_name, + role_group_name, + )) .namespace(&self.cluster.namespace) .with_labels(self.build_recommended_labels(role_group_name)) .build(); diff --git a/rust/operator-binary/src/controller/update_status.rs b/rust/operator-binary/src/controller/update_status.rs new file mode 100644 index 0000000..2984a60 --- /dev/null +++ b/rust/operator-binary/src/controller/update_status.rs @@ -0,0 +1,68 @@ +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + client::Client, + status::condition::{ + compute_conditions, operations::ClusterOperationsConditionBuilder, + statefulset::StatefulSetConditionBuilder, + }, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use super::{Applied, Resources}; +use crate::{ + OPERATOR_NAME, + crd::v1alpha1::{self, OpenSearchClusterStatus}, +}; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to create cluster resources"))] + CreateClusterResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphanedResources { + source: stackable_operator::cluster_resources::Error, + }, + + #[snafu(display("failed to update status"))] + UpdateStatus { + source: stackable_operator::client::Error, + }, +} + +type Result = std::result::Result; + +pub async fn update_status( + client: &Client, + cluster: &v1alpha1::OpenSearchCluster, + applied_resources: Resources, +) -> Result<()> { + let mut stateful_set_condition_builder = StatefulSetConditionBuilder::default(); + for stateful_set in applied_resources.stateful_sets { + stateful_set_condition_builder.add(stateful_set); + } + + let cluster_operation_cond_builder = + ClusterOperationsConditionBuilder::new(&cluster.spec.cluster_operation); + + let status = OpenSearchClusterStatus { + conditions: compute_conditions( + cluster, + &[ + &stateful_set_condition_builder, + &cluster_operation_cond_builder, + ], + ), + discovery_hash: None, + }; + + client + .apply_patch_status(OPERATOR_NAME, cluster, &status) + .await + .context(UpdateStatusSnafu)?; + + Ok(()) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 1d1b006..0bfe256 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -2,15 +2,13 @@ use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::{cluster_operation::ClusterOperation, product_image_selection::ProductImage}, config::{fragment::Fragment, merge::Merge}, - kube::{CustomResource, ResourceExt}, + kube::CustomResource, role_utils::Role, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, versioned::versioned, }; -use crate::framework::ToLabelValue; - #[versioned(version(name = "v1alpha1"))] pub mod versioned { /// A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. @@ -63,13 +61,6 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { } } -impl ToLabelValue for v1alpha1::OpenSearchCluster { - fn to_label_value(&self) -> String { - // opinionated! - self.name_unchecked() - } -} - // TODO Perhaps rename to InstanceConfig #[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 8a2eb31..b747de8 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -3,17 +3,19 @@ use std::{fmt::Display, str::FromStr}; +use kvp::label::LABEL_VALUE_MAX_LENGTH; use snafu::{ResultExt, Snafu, ensure}; use stackable_operator::kvp::LabelValue; use strum::{EnumDiscriminants, IntoStaticStr}; +pub mod cluster_resources; pub mod kvp; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { #[snafu(display("maximum length exceeded"))] - LengthExceeded { length: u32, max_length: u32 }, + LengthExceeded { length: usize, max_length: usize }, #[snafu(display("object name not RFC 1123 compliant"))] InvalidObjectName { @@ -26,40 +28,53 @@ pub enum Error { }, } -pub trait ToObjectName { +// according to RFC 1123 +const _OBJECT_NAME_MAX_LENGTH: usize = 253; + +// useful? +pub trait HasObjectName { fn to_object_name(&self) -> String; } -pub trait ToLabelValue { +pub trait HasNamespace { + fn to_namespace(&self) -> String; +} + +pub trait HasUid { + fn to_uid(&self) -> String; +} + +pub trait IsLabelValue { fn to_label_value(&self) -> String; } +/// max_length must not exceed 63! This cannot be checked at compile time. macro_rules! object_name { ($name:ident, $max_length:literal) => { /// Bla - #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] - pub struct $name { - value: String, - // could be somehow static + #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] + pub struct $name(String); + + impl $name { // type arithmetic would be better - max_length: u32, + pub const MAX_LENGTH: usize = $max_length; } impl Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.value.fmt(f) + self.0.fmt(f) } } - impl ToObjectName for $name { + impl HasObjectName for $name { fn to_object_name(&self) -> String { - self.value.clone() + self.0.clone() } } - impl ToLabelValue for $name { + impl IsLabelValue for $name { fn to_label_value(&self) -> String { - self.value.clone() + self.0.clone() } } @@ -67,12 +82,12 @@ macro_rules! object_name { type Err = Error; fn from_str(s: &str) -> std::result::Result { - let length = s.len() as u32; + let length = s.len() as usize; ensure!( - length < $max_length, + /* length <= LABEL_VALUE_MAX_LENGTH && */ length <= $name::MAX_LENGTH, LengthExceededSnafu { length, - max_length: $max_length + max_length: $name::MAX_LENGTH, } ); @@ -81,54 +96,52 @@ macro_rules! object_name { LabelValue::from_str(s).context(InvalidLabelValueSnafu)?; - Ok(Self { - value: s.to_owned(), - max_length: $max_length, - }) + Ok(Self(s.to_owned())) } } }; } -object_name!(AppName, 63u32); -object_name!(AppVersion, 63u32); -object_name!(ClusterName, 63u32); -object_name!(ControllerName, 63u32); -object_name!(OperatorName, 63u32); -object_name!(RoleGroupName, 63u32); -object_name!(RoleName, 63u32); -object_name!(QualifiedRoleGroupName, 250u32); - -pub fn qualified_role_group_name( +// There are compile time checks elsewhere ... +// TODO Do not automatically derive ToObjectName and ToLabelValue. +// Version is no object name! +object_name!(AppName, 54); +object_name!(AppVersion, 63); +object_name!(ClusterName, 63); +object_name!(ControllerName, 63); +object_name!(OperatorName, 63); +object_name!(RoleGroupName, 63); +object_name!(RoleName, 63); + +pub fn to_qualified_role_group_name( cluster_name: &ClusterName, role_name: &RoleName, role_group_name: &RoleGroupName, -) -> QualifiedRoleGroupName { - // This assertion is already checked when running the unit test below, so it is not expected to - // fail at runtime of the operator. - assert!( - cluster_name.max_length + role_name.max_length + role_group_name.max_length < 250, +) -> String { + // Compile time check + const _: () = assert!( + ClusterName::MAX_LENGTH + RoleName::MAX_LENGTH + RoleGroupName::MAX_LENGTH + <= _OBJECT_NAME_MAX_LENGTH - 3 /* dashes */ - 4, /* digits */ "The maximum lengths of the cluster name, role name and role group name must be defined so that the combination of these names (including separators and the sequential pod number) is also a valid object name with a maximum of 263 characters (see RFC 1123)" ); - QualifiedRoleGroupName::from_str(&format!( + format!( "{}-{}-{}", cluster_name.to_object_name(), role_name.to_object_name(), role_group_name.to_object_name() - )) - .expect("") + ) } #[cfg(test)] mod tests { use std::str::FromStr; - use super::{ClusterName, RoleGroupName, RoleName, qualified_role_group_name}; - use crate::framework::{AppName, ToObjectName}; + use super::{ClusterName, RoleGroupName, RoleName, to_qualified_role_group_name}; + use crate::framework::AppName; #[test] - fn test_typed_string_constraints() { + fn test_object_name_constraints() { assert!(AppName::from_str("valid-role-group-name").is_ok()); assert!(AppName::from_str("invalid-character: /").is_err()); assert!( @@ -141,7 +154,7 @@ mod tests { #[test] fn test_qualified_role_group_name() { - let qualified_role_group_name = qualified_role_group_name( + let qualified_role_group_name = to_qualified_role_group_name( &ClusterName::from_str("test-cluster").expect("should be a valid cluster name"), &RoleName::from_str("data-nodes").expect("should be a valid role name"), &RoleGroupName::from_str("ssd-storage").expect("should be a valid role group name"), @@ -149,7 +162,7 @@ mod tests { assert_eq!( "test-cluster-data-nodes-ssd-storage", - qualified_role_group_name.to_object_name() + qualified_role_group_name ); } } diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs new file mode 100644 index 0000000..5560780 --- /dev/null +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -0,0 +1,39 @@ +use stackable_operator::{ + cluster_resources::{ClusterResourceApplyStrategy, ClusterResources}, + k8s_openapi::api::core::v1::ObjectReference, +}; + +use super::{ + AppName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, +}; +use crate::framework::kvp::label::LABEL_VALUE_MAX_LENGTH; + +pub fn cluster_resources_new( + app_name: &AppName, + operator_name: &OperatorName, + controller_name: &ControllerName, + cluster: &(impl HasObjectName + HasNamespace + HasUid), + apply_strategy: ClusterResourceApplyStrategy, +) -> ClusterResources { + // ClusterResources::new creates a label value from the given app name by appending + // `-operator`. For the resulting label value to be valid, it must not exceed 63 characters. + // Check at compile time that AppName::MAX_LENGTH is defined accordingly. + const _: () = assert!( + AppName::MAX_LENGTH <= LABEL_VALUE_MAX_LENGTH - "-operator".len(), + "The label value `-operator` must not exceed 63 characters." + ); + + ClusterResources::new( + &app_name.to_label_value(), + &operator_name.to_label_value(), + &controller_name.to_label_value(), + &ObjectReference { + name: Some(cluster.to_object_name()), + namespace: Some(cluster.to_namespace()), + uid: Some(cluster.to_uid()), + ..Default::default() + }, + apply_strategy, + ) + .expect("") +} diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs index fe8e413..ebaeb8d 100644 --- a/rust/operator-binary/src/framework/kvp/label.rs +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -4,11 +4,13 @@ use stackable_operator::{ }; use crate::framework::{ - AppName, AppVersion, ControllerName, OperatorName, RoleGroupName, RoleName, ToLabelValue, + AppName, AppVersion, ControllerName, IsLabelValue, OperatorName, RoleGroupName, RoleName, }; +pub const LABEL_VALUE_MAX_LENGTH: usize = 63; + pub fn recommended_labels( - owner: &(impl Resource + ToLabelValue), + owner: &(impl Resource + IsLabelValue), app_name: &AppName, app_version: &AppVersion, operator_name: &OperatorName, @@ -30,7 +32,7 @@ pub fn recommended_labels( } pub fn role_group_selector( - owner: &(impl Resource + ToLabelValue), + owner: &(impl Resource + IsLabelValue), app_name: &AppName, role_name: &RoleName, role_group_name: &RoleGroupName, From 48d20e5c1e810562b74236611fa0811d87b09142 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 11 Jun 2025 17:02:44 +0200 Subject: [PATCH 08/35] Add ContextNames structure --- Cargo.lock | 236 +++++++------- Cargo.nix | 303 ++++++++---------- Cargo.toml | 1 - rust/operator-binary/Cargo.toml | 1 - rust/operator-binary/src/controller.rs | 60 ++-- rust/operator-binary/src/controller/apply.rs | 14 +- rust/operator-binary/src/controller/build.rs | 39 +-- .../src/controller/update_status.rs | 5 +- rust/operator-binary/src/main.rs | 23 +- 9 files changed, 315 insertions(+), 367 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64af4cd..001739c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -77,33 +77,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -147,7 +147,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -158,7 +158,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -229,9 +229,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496" +checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" dependencies = [ "fastrand", "gloo-timers", @@ -253,12 +253,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -307,9 +301,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytes" @@ -319,9 +313,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.24" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", @@ -330,9 +324,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" @@ -349,9 +343,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -359,9 +353,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -371,27 +365,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -521,7 +515,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -532,7 +526,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -543,7 +537,7 @@ checksum = "b9b6483c2bbed26f97861cf57651d4f2b731964a28cd2257f934a4b452480d21" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -572,7 +566,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -593,7 +587,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -631,7 +625,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -675,7 +669,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -724,9 +718,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -815,7 +809,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -867,7 +861,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -946,9 +940,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -957,11 +951,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "headers-core", "http", @@ -1094,9 +1088,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", @@ -1125,11 +1119,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -1301,7 +1295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.15.4", ] [[package]] @@ -1413,7 +1407,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa60a41b57ae1a0a071af77dbcf89fc9819cfe66edaf2beeb204c34459dcf0b2" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "schemars", "serde", @@ -1450,7 +1444,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cb276b85b6e94ded00ac8ea2c68fcf4697ea0553cb25fddc35d4a0ab718db8d" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "chrono", "either", @@ -1511,7 +1505,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -1526,7 +1520,7 @@ dependencies = [ "backon", "educe", "futures 0.3.31", - "hashbrown 0.15.3", + "hashbrown 0.15.4", "hostname", "json-patch", "k8s-openapi", @@ -1585,9 +1579,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1616,9 +1610,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mime" @@ -1628,9 +1622,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -1642,7 +1636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1813,9 +1807,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1823,9 +1817,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1840,7 +1834,7 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] @@ -1881,7 +1875,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -1912,7 +1906,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2002,7 +1996,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2134,11 +2128,11 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.17" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f27698fc5799d2a281528a908bd874973953ca2e7bc2311637e10c263c723b" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -2148,11 +2142,8 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "serde", @@ -2185,9 +2176,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustls" @@ -2301,7 +2292,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2388,7 +2379,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2399,7 +2390,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2506,9 +2497,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snafu" @@ -2549,7 +2540,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2574,7 +2565,6 @@ version = "0.0.0-dev" dependencies = [ "built", "clap", - "const_format", "futures 0.3.31", "serde", "serde_json", @@ -2630,7 +2620,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2694,7 +2684,7 @@ dependencies = [ "kube", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2722,7 +2712,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2744,9 +2734,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" dependencies = [ "proc-macro2", "quote", @@ -2770,7 +2760,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2799,7 +2789,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2810,7 +2800,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2890,7 +2880,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -2935,7 +2925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-trait", - "base64 0.22.1", + "base64", "bytes", "flate2", "http", @@ -2994,11 +2984,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "base64 0.22.1", + "base64", "bitflags", "bytes", "futures-util", @@ -3051,20 +3041,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -3231,9 +3221,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -3266,7 +3256,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", "wasm-bindgen-shared", ] @@ -3301,7 +3291,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3378,7 +3368,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -3389,7 +3379,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -3539,7 +3529,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", "synstructure", ] @@ -3560,7 +3550,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] [[package]] @@ -3580,7 +3570,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", "synstructure", ] @@ -3620,5 +3610,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.102", ] diff --git a/Cargo.nix b/Cargo.nix index 8e9b3de..a557c35 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -120,18 +120,17 @@ rec { }; "adler2" = rec { crateName = "adler2"; - version = "2.0.0"; + version = "2.0.1"; edition = "2021"; - sha256 = "09r6drylvgy8vv8k20lnbvwq8gp09h7smfn6h1rxsy15pgh629si"; + sha256 = "1ymy18s9hs7ya1pjc9864l30wk8p2qfqdi7mhhcc5nfakxbij09j"; authors = [ "Jonas Schievink " "oyvindln " ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "std" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "ahash" = rec { @@ -253,9 +252,9 @@ rec { }; "anstream" = rec { crateName = "anstream"; - version = "0.6.18"; + version = "0.6.19"; edition = "2021"; - sha256 = "16sjk4x3ns2c3ya1x28a44kh6p47c7vhk27251i015hik1lm7k4a"; + sha256 = "0crr9a207dyn8k66xgvhvmlxm9raiwpss3syfa35c6265s9z26ih"; dependencies = [ { name = "anstyle"; @@ -298,9 +297,9 @@ rec { }; "anstyle" = rec { crateName = "anstyle"; - version = "1.0.10"; + version = "1.0.11"; edition = "2021"; - sha256 = "1yai2vppmd7zlvlrp9grwll60knrmscalf8l2qpfz8b7y5lkpk2m"; + sha256 = "1gbbzi0zbgff405q14v8hhpi1kz2drzl9a75r3qhks47lindjbl6"; features = { "default" = [ "std" ]; }; @@ -308,9 +307,9 @@ rec { }; "anstyle-parse" = rec { crateName = "anstyle-parse"; - version = "0.2.6"; + version = "0.2.7"; edition = "2021"; - sha256 = "1acqayy22fwzsrvr6n0lz6a4zvjjcvgr5sm941m7m0b2fr81cb9v"; + sha256 = "1hhmkkfr95d462b3zf6yl2vfzdqfy5726ya572wwg8ha9y148xjf"; libName = "anstyle_parse"; dependencies = [ { @@ -328,9 +327,9 @@ rec { }; "anstyle-query" = rec { crateName = "anstyle-query"; - version = "1.1.2"; + version = "1.1.3"; edition = "2021"; - sha256 = "036nm3lkyk43xbps1yql3583fp4hg3b1600is7mcyxs1gzrpm53r"; + sha256 = "1sgs2hq54wayrmpvy784ww2ccv9f8yhhpasv12z872bx0jvdx2vc"; libName = "anstyle_query"; dependencies = [ { @@ -344,9 +343,9 @@ rec { }; "anstyle-wincon" = rec { crateName = "anstyle-wincon"; - version = "3.0.8"; + version = "3.0.9"; edition = "2021"; - sha256 = "1ykkvih20kykgfix7j8y74av90m2v8ji72hv373f8vmx659dx036"; + sha256 = "10n8mcgr89risdf35i73zc67aaa392bhggwzqlri1fv79297ags0"; libName = "anstyle_wincon"; dependencies = [ { @@ -458,7 +457,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" "visit-mut" ]; } ]; @@ -485,7 +484,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "clone-impls" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -761,9 +760,9 @@ rec { }; "backon" = rec { crateName = "backon"; - version = "1.5.0"; + version = "1.5.1"; edition = "2021"; - sha256 = "15k4p6xyxi4lkiyw5yxrmcws3wwnwjacgcqqmd2dvfldnyqm02zx"; + sha256 = "1rwr3ycl69vycyaxrhwzfjcwyqf7pawfq9zi88n4l9ks6pssybih"; dependencies = [ { name = "fastrand"; @@ -869,22 +868,7 @@ rec { }; resolvedDefaultFeatures = [ "default" "std" ]; }; - "base64 0.21.7" = rec { - crateName = "base64"; - version = "0.21.7"; - edition = "2018"; - sha256 = "0rw52yvsk75kar9wgqfwgb414kvil1gn7mqkrhn9zf1537mpsacx"; - authors = [ - "Alice Maz " - "Marshall Pierce " - ]; - features = { - "default" = [ "std" ]; - "std" = [ "alloc" ]; - }; - resolvedDefaultFeatures = [ "alloc" "default" "std" ]; - }; - "base64 0.22.1" = rec { + "base64" = rec { crateName = "base64"; version = "0.22.1"; edition = "2018"; @@ -1005,14 +989,15 @@ rec { }; "bumpalo" = rec { crateName = "bumpalo"; - version = "3.17.0"; + version = "3.18.1"; edition = "2021"; - sha256 = "1gxxsn2fsjmv03g8p3m749mczv2k4m8xspifs5l7bcx0vx3gna0n"; + sha256 = "1vmfniqr484l4ffkf0056g6hakncr7kdh11hyggh9kc7c5nvfgbr"; authors = [ "Nick Fitzgerald " ]; features = { "allocator-api2" = [ "dep:allocator-api2" ]; + "bench_allocator_api" = [ "allocator_api" "blink-alloc/nightly" ]; "serde" = [ "dep:serde" ]; }; resolvedDefaultFeatures = [ "default" ]; @@ -1035,9 +1020,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.2.24"; + version = "1.2.26"; edition = "2018"; - sha256 = "1irvbn8y9sg6f1070yg5469fxk5c3ximh24ds04kph21w0xmsn8n"; + sha256 = "1b5g9ln7a2imwhrvfi77qbmj7gxsg0xihrlvarrg71wbk0hmwslm"; authors = [ "Alex Crichton " ]; @@ -1067,17 +1052,16 @@ rec { }; "cfg-if" = rec { crateName = "cfg-if"; - version = "1.0.0"; + version = "1.0.1"; edition = "2018"; - sha256 = "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds"; + sha256 = "0s0jr5j797q1vqjcd41l0v5izlmlqm7lxy512b418xz5r65mfmcm"; libName = "cfg_if"; authors = [ "Alex Crichton " ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "chrono" = rec { @@ -1143,10 +1127,10 @@ rec { }; "clap" = rec { crateName = "clap"; - version = "4.5.39"; + version = "4.5.40"; edition = "2021"; crateBin = []; - sha256 = "17raqwxkhhhm80iyblp1v83fvpddkg7rgqr2cjsmz3p6kczfcq7x"; + sha256 = "03widrb9d7a0bka6lsf9r9f65zhfbkdkhm8iryycx1c63mx8idj0"; dependencies = [ { name = "clap_builder"; @@ -1185,9 +1169,9 @@ rec { }; "clap_builder" = rec { crateName = "clap_builder"; - version = "4.5.39"; + version = "4.5.40"; edition = "2021"; - sha256 = "0lggb5vscs21jliisvjjphcazzb1iw8347yp42wbwazpl6967k49"; + sha256 = "17pmcjwk6rbkizj4y5vlhrnr7b5n1ffjgh75pj66j34zrq46rip0"; dependencies = [ { name = "anstream"; @@ -1224,9 +1208,9 @@ rec { }; "clap_derive" = rec { crateName = "clap_derive"; - version = "4.5.32"; + version = "4.5.40"; edition = "2021"; - sha256 = "1mqcag8qapb5yhygg2hi153kzmbf7w5hqp3nl3fvl5cn4yp6l5q9"; + sha256 = "1kjp4928wy132inisss42750rzv0wasvbbf10w98agfcwix99iyj"; procMacro = true; dependencies = [ { @@ -1243,7 +1227,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } ]; @@ -1256,16 +1240,16 @@ rec { }; "clap_lex" = rec { crateName = "clap_lex"; - version = "0.7.4"; + version = "0.7.5"; edition = "2021"; - sha256 = "19nwfls5db269js5n822vkc8dw0wjq2h1wf0hgr06ld2g52d2spl"; + sha256 = "0xb6pjza43irrl99axbhs12pxq4sr8x7xd36p703j57f5i3n2kxr"; }; "colorchoice" = rec { crateName = "colorchoice"; - version = "1.0.3"; + version = "1.0.4"; edition = "2021"; - sha256 = "1439m3r3jy3xqck8aa13q658visn71ki76qa93cy55wkmalwlqsv"; + sha256 = "0x8ymkz1xr77rcj1cfanhf416pc4v681gmkc9dzb3jqja7f62nxh"; }; "concurrent-queue" = rec { @@ -1622,7 +1606,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" "extra-traits" ]; } ]; @@ -1652,7 +1636,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; @@ -1678,7 +1662,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" "visit-mut" ]; } ]; @@ -1780,7 +1764,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; features = { @@ -1856,7 +1840,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; features = { @@ -1949,13 +1933,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } ]; @@ -2058,7 +2042,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; features = { @@ -2192,9 +2176,9 @@ rec { }; "flate2" = rec { crateName = "flate2"; - version = "1.1.1"; + version = "1.1.2"; edition = "2018"; - sha256 = "1kpycx57dqpkr3vp53b4nq75p9mflh0smxy8hkys4v4ndvkr5vbw"; + sha256 = "07abz7v50lkdr5fjw8zaw2v8gm2vbppc0f7nqm8x3v3gb6wpsgaa"; authors = [ "Alex Crichton " "Josh Triplett " @@ -2461,7 +2445,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } ]; @@ -2632,7 +2616,7 @@ rec { } { name = "wasi"; - packageId = "wasi 0.11.0+wasi-snapshot-preview1"; + packageId = "wasi 0.11.1+wasi-snapshot-preview1"; usesDefaultFeatures = false; target = { target, features }: ("wasi" == target."os" or null); } @@ -2926,11 +2910,11 @@ rec { }; resolvedDefaultFeatures = [ "raw" ]; }; - "hashbrown 0.15.3" = rec { + "hashbrown 0.15.4" = rec { crateName = "hashbrown"; - version = "0.15.3"; + version = "0.15.4"; edition = "2021"; - sha256 = "1cwfw1yzkvsqkhmkg5igdvgsl8a0wyi716cn83k2j8h09ma6rcl4"; + sha256 = "1mg045sm1nm00cwjm7ndi80hcmmv1v3z7gnapxyhd9qxc62sqwar"; authors = [ "Amanieu d'Antras " ]; @@ -2958,30 +2942,29 @@ rec { features = { "alloc" = [ "dep:alloc" ]; "allocator-api2" = [ "dep:allocator-api2" ]; - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "default-hasher" "inline-more" "allocator-api2" "equivalent" "raw-entry" ]; "default-hasher" = [ "dep:foldhash" ]; "equivalent" = [ "dep:equivalent" ]; "nightly" = [ "bumpalo/allocator_api" ]; "rayon" = [ "dep:rayon" ]; - "rustc-dep-of-std" = [ "nightly" "core" "compiler_builtins" "alloc" "rustc-internal-api" ]; + "rustc-dep-of-std" = [ "nightly" "core" "alloc" "rustc-internal-api" ]; "serde" = [ "dep:serde" ]; }; resolvedDefaultFeatures = [ "allocator-api2" "default" "default-hasher" "equivalent" "inline-more" "raw-entry" ]; }; "headers" = rec { crateName = "headers"; - version = "0.4.0"; - edition = "2015"; - sha256 = "1abari69kjl2yv2dg06g2x17qgd1a20xp7aqmmg2vfhcppk0c89j"; + version = "0.4.1"; + edition = "2018"; + sha256 = "1sr4zygaq1b2f0k7b5l8vx5vp05wvd82w7vpavgvr52xvdd4scdk"; authors = [ "Sean McArthur " ]; dependencies = [ { name = "base64"; - packageId = "base64 0.21.7"; + packageId = "base64"; } { name = "bytes"; @@ -3400,9 +3383,9 @@ rec { }; "hyper-rustls" = rec { crateName = "hyper-rustls"; - version = "0.27.6"; + version = "0.27.7"; edition = "2021"; - sha256 = "0va008pmz5h062wnh2h08d3r3iizvqnw68k5ji8frp0vw6aib803"; + sha256 = "0n6g8998szbzhnvcs1b7ibn745grxiqmlpg53xz206v826v3xjg3"; libName = "hyper_rustls"; dependencies = [ { @@ -3544,9 +3527,9 @@ rec { }; "hyper-util" = rec { crateName = "hyper-util"; - version = "0.1.13"; + version = "0.1.14"; edition = "2021"; - sha256 = "1s06md3mq6v6w2zqq0qfag2hw8drsvmxpiqd4mwcl7njnfv97hmi"; + sha256 = "1nqvf5azmv8p7hs5ghjlbgfya7xaafq377vppdazxbq8zzdxybyw"; libName = "hyper_util"; authors = [ "Sean McArthur " @@ -3554,7 +3537,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; optional = true; } { @@ -4153,7 +4136,7 @@ rec { } { name = "hashbrown"; - packageId = "hashbrown 0.15.3"; + packageId = "hashbrown 0.15.4"; usesDefaultFeatures = false; } ]; @@ -4458,7 +4441,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; usesDefaultFeatures = false; features = [ "alloc" ]; } @@ -4622,7 +4605,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; optional = true; } { @@ -4967,7 +4950,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "extra-traits" ]; } ]; @@ -5022,7 +5005,7 @@ rec { } { name = "hashbrown"; - packageId = "hashbrown 0.15.3"; + packageId = "hashbrown 0.15.4"; } { name = "hostname"; @@ -5233,9 +5216,9 @@ rec { }; "lock_api" = rec { crateName = "lock_api"; - version = "0.4.12"; + version = "0.4.13"; edition = "2021"; - sha256 = "05qvxa6g27yyva25a5ghsg85apdxkvr77yhkyhapj6r8vnf8pbq7"; + sha256 = "0rd73p4299mjwl4hhlfj9qr88v3r0kc8s1nszkfmnq2ky43nb4wn"; authors = [ "Amanieu d'Antras " ]; @@ -5312,19 +5295,18 @@ rec { }; "memchr" = rec { crateName = "memchr"; - version = "2.7.4"; + version = "2.7.5"; edition = "2021"; - sha256 = "18z32bhxrax0fnjikv475z7ii718hq457qwmaryixfxsl2qrmjkq"; + sha256 = "1h2bh2jajkizz04fh047lpid5wgw2cr9igpkdhl3ibzscpd858ij"; authors = [ "Andrew Gallant " "bluss" ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "std" ]; "logging" = [ "dep:log" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; "std" = [ "alloc" ]; "use_std" = [ "std" ]; }; @@ -5342,9 +5324,9 @@ rec { }; "miniz_oxide" = rec { crateName = "miniz_oxide"; - version = "0.8.8"; + version = "0.8.9"; edition = "2021"; - sha256 = "0al9iy33flfgxawj789w2c8xxwg1n2r5vv6m6p5hl2fvd2vlgriv"; + sha256 = "05k3pdg8bjjzayq3rf0qhpirq9k37pxnasfn4arbs17phqn6m9qz"; authors = [ "Frommi " "oyvindln " @@ -5359,10 +5341,9 @@ rec { ]; features = { "alloc" = [ "dep:alloc" ]; - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "with-alloc" ]; - "rustc-dep-of-std" = [ "core" "alloc" "compiler_builtins" "adler2/rustc-dep-of-std" ]; + "rustc-dep-of-std" = [ "core" "alloc" "adler2/rustc-dep-of-std" ]; "serde" = [ "dep:serde" ]; "simd" = [ "simd-adler32" ]; "simd-adler32" = [ "dep:simd-adler32" ]; @@ -5397,7 +5378,7 @@ rec { } { name = "wasi"; - packageId = "wasi 0.11.0+wasi-snapshot-preview1"; + packageId = "wasi 0.11.1+wasi-snapshot-preview1"; target = { target, features }: ("wasi" == target."os" or null); } { @@ -6036,9 +6017,9 @@ rec { }; "parking_lot" = rec { crateName = "parking_lot"; - version = "0.12.3"; + version = "0.12.4"; edition = "2021"; - sha256 = "09ws9g6245iiq8z975h8ycf818a66q3c6zv4b5h8skpm7hc1igzi"; + sha256 = "04sab1c7304jg8k0d5b2pxbj1fvgzcf69l3n2mfpkdb96vs8pmbh"; authors = [ "Amanieu d'Antras " ]; @@ -6063,9 +6044,9 @@ rec { }; "parking_lot_core" = rec { crateName = "parking_lot_core"; - version = "0.9.10"; + version = "0.9.11"; edition = "2021"; - sha256 = "1y3cf9ld9ijf7i4igwzffcn0xl16dxyn4c5bwgjck1dkgabiyh0y"; + sha256 = "19g4d6m5k4ggacinqprnn8xvdaszc3y5smsmbz1adcdmaqm8v0xw"; authors = [ "Amanieu d'Antras " ]; @@ -6112,7 +6093,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; usesDefaultFeatures = false; features = [ "alloc" ]; } @@ -6236,7 +6217,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; features = { @@ -6309,7 +6290,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "parsing" "printing" "clone-impls" "proc-macro" "full" "visit-mut" ]; } @@ -6544,7 +6525,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "extra-traits" ]; } ]; @@ -6975,16 +6956,16 @@ rec { }; "reqwest" = rec { crateName = "reqwest"; - version = "0.12.17"; + version = "0.12.20"; edition = "2021"; - sha256 = "0fvj7hk0rq9p2qqw4yrfr99kk5vlv25r12jjh6id56apzjc7dwn3"; + sha256 = "04qqxghqszjxk4pl4vxa5qlwinkfx0vvjkk10vv2n3hkv6blrgza"; authors = [ "Sean McArthur " ]; dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; } { name = "bytes"; @@ -7033,11 +7014,6 @@ rec { target = { target, features }: (!("wasm32" == target."arch" or null)); features = [ "http1" "client" "client-legacy" "client-proxy" "tokio" ]; } - { - name = "ipnet"; - packageId = "ipnet"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } { name = "js-sys"; packageId = "js-sys"; @@ -7048,16 +7024,6 @@ rec { packageId = "log"; target = { target, features }: (!("wasm32" == target."arch" or null)); } - { - name = "mime"; - packageId = "mime"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } - { - name = "once_cell"; - packageId = "once_cell"; - target = { target, features }: (!("wasm32" == target."arch" or null)); - } { name = "percent-encoding"; packageId = "percent-encoding"; @@ -7190,16 +7156,16 @@ rec { "__tls" = [ "dep:rustls-pki-types" "tokio/io-util" ]; "blocking" = [ "dep:futures-channel" "futures-channel?/sink" "dep:futures-util" "futures-util?/io" "futures-util?/sink" "tokio/sync" ]; "brotli" = [ "dep:async-compression" "async-compression?/brotli" "dep:futures-util" "dep:tokio-util" ]; - "charset" = [ "dep:encoding_rs" ]; + "charset" = [ "dep:encoding_rs" "dep:mime" ]; "cookies" = [ "dep:cookie_crate" "dep:cookie_store" ]; "default" = [ "default-tls" "charset" "http2" "system-proxy" ]; "default-tls" = [ "dep:hyper-tls" "dep:native-tls-crate" "__tls" "dep:tokio-native-tls" ]; "deflate" = [ "dep:async-compression" "async-compression?/zlib" "dep:futures-util" "dep:tokio-util" ]; "gzip" = [ "dep:async-compression" "async-compression?/gzip" "dep:futures-util" "dep:tokio-util" ]; "h2" = [ "dep:h2" ]; - "hickory-dns" = [ "dep:hickory-resolver" ]; + "hickory-dns" = [ "dep:hickory-resolver" "dep:once_cell" ]; "http2" = [ "h2" "hyper/http2" "hyper-util/http2" "hyper-rustls?/http2" ]; - "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "dep:slab" "dep:futures-channel" "tokio/macros" ]; + "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "dep:slab" "tokio/macros" ]; "json" = [ "dep:serde_json" ]; "macos-system-configuration" = [ "system-proxy" ]; "multipart" = [ "dep:mime_guess" "dep:futures-util" ]; @@ -7214,7 +7180,6 @@ rec { "rustls-tls-no-provider" = [ "rustls-tls-manual-roots-no-provider" ]; "rustls-tls-webpki-roots" = [ "rustls-tls-webpki-roots-no-provider" "__rustls-ring" ]; "rustls-tls-webpki-roots-no-provider" = [ "dep:webpki-roots" "hyper-rustls?/webpki-tokio" "__rustls" ]; - "socks" = [ "dep:tokio-socks" ]; "stream" = [ "tokio/fs" "dep:futures-util" "dep:tokio-util" "dep:wasm-streams" ]; "system-proxy" = [ "hyper-util/client-proxy-system" ]; "zstd" = [ "dep:async-compression" "async-compression?/zstd" "dep:futures-util" "dep:tokio-util" ]; @@ -7284,17 +7249,16 @@ rec { }; "rustc-demangle" = rec { crateName = "rustc-demangle"; - version = "0.1.24"; + version = "0.1.25"; edition = "2015"; - sha256 = "07zysaafgrkzy2rjgwqdj2a8qdpsm6zv6f5pgpk9x0lm40z9b6vi"; + sha256 = "0kxq6m0drr40434ch32j31dkg00iaf4zxmqg7sqxajhcz0wng7lq"; libName = "rustc_demangle"; authors = [ "Alex Crichton " ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; - "rustc-dep-of-std" = [ "core" "compiler_builtins" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "rustls" = rec { @@ -7645,7 +7609,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "extra-traits" ]; } ]; @@ -7899,7 +7863,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -7931,7 +7895,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -8218,9 +8182,9 @@ rec { }; "smallvec" = rec { crateName = "smallvec"; - version = "1.15.0"; + version = "1.15.1"; edition = "2018"; - sha256 = "1sgfw8z729nlxk8k13dhs0a762wnaxmlx70a7xlf3wz989bjh5w9"; + sha256 = "00xxdxxpgyq5vjnpljvkmy99xij5rxgh913ii1v16kzynnivgcb7"; authors = [ "The Servo Project Developers" ]; @@ -8356,7 +8320,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } ]; @@ -8424,10 +8388,6 @@ rec { name = "clap"; packageId = "clap"; } - { - name = "const_format"; - packageId = "const_format"; - } { name = "futures"; packageId = "futures 0.3.31"; @@ -8664,7 +8624,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; @@ -8927,7 +8887,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; features = { @@ -8998,7 +8958,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "parsing" ]; } ]; @@ -9051,11 +9011,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "full" "parsing" "printing" "proc-macro" "quote" ]; }; - "syn 2.0.101" = rec { + "syn 2.0.102" = rec { crateName = "syn"; - version = "2.0.101"; + version = "2.0.102"; edition = "2021"; - sha256 = "1brwsh7fn3bnbj50d2lpwy9akimzb3lghz0ai89j8fhvjkybgqlc"; + sha256 = "0qh4v2nj61y82cc713fakjckhmwyvllq9n0gpmcg147sjjppsfgn"; authors = [ "David Tolnay " ]; @@ -9127,7 +9087,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "derive" "parsing" "printing" "clone-impls" "visit" "extra-traits" ]; } @@ -9194,7 +9154,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; @@ -9220,7 +9180,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; } ]; @@ -9518,7 +9478,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } ]; @@ -9680,7 +9640,7 @@ rec { } { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; } { name = "bytes"; @@ -10026,9 +9986,9 @@ rec { }; "tower-http" = rec { crateName = "tower-http"; - version = "0.6.4"; + version = "0.6.6"; edition = "2018"; - sha256 = "0bladfcd75dkh3ikmf2m4f971nc0zn8b5pb9mdbryym27hhhrnqg"; + sha256 = "1wh51y4rf03f91c6rvli6nwzsarx7097yx6sqlm75ag27pbjzj5d"; libName = "tower_http"; authors = [ "Tower Maintainers " @@ -10036,7 +9996,7 @@ rec { dependencies = [ { name = "base64"; - packageId = "base64 0.22.1"; + packageId = "base64"; optional = true; } { @@ -10132,7 +10092,7 @@ rec { "decompression-gzip" = [ "async-compression/gzip" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; "decompression-zstd" = [ "async-compression/zstd" "futures-core" "dep:http-body" "dep:http-body-util" "tokio-util" "tokio" ]; "follow-redirect" = [ "futures-util" "dep:http-body" "iri-string" "tower/util" ]; - "fs" = [ "futures-util" "dep:http-body" "dep:http-body-util" "tokio/fs" "tokio-util/io" "tokio/io-util" "dep:http-range-header" "mime_guess" "mime" "percent-encoding" "httpdate" "set-status" "futures-util/alloc" "tracing" ]; + "fs" = [ "futures-core" "futures-util" "dep:http-body" "dep:http-body-util" "tokio/fs" "tokio-util/io" "tokio/io-util" "dep:http-range-header" "mime_guess" "mime" "percent-encoding" "httpdate" "set-status" "futures-util/alloc" "tracing" ]; "full" = [ "add-extension" "auth" "catch-panic" "compression-full" "cors" "decompression-full" "follow-redirect" "fs" "limit" "map-request-body" "map-response-body" "metrics" "normalize-path" "propagate-header" "redirect" "request-id" "sensitive-headers" "set-header" "set-status" "timeout" "trace" "util" "validate-request" ]; "futures-core" = [ "dep:futures-core" ]; "futures-util" = [ "dep:futures-util" ]; @@ -10263,9 +10223,9 @@ rec { }; "tracing-attributes" = rec { crateName = "tracing-attributes"; - version = "0.1.28"; + version = "0.1.29"; edition = "2018"; - sha256 = "0v92l9cxs42rdm4m5hsa8z7ln1xsiw1zc2iil8c6k7lzq0jf2nir"; + sha256 = "0qpn22v675pbgmrkjsx3abj6lr5s12v4wi77hv9rjsvgkk7zn7qv"; procMacro = true; libName = "tracing_attributes"; authors = [ @@ -10284,7 +10244,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ]; } @@ -10294,9 +10254,9 @@ rec { }; "tracing-core" = rec { crateName = "tracing-core"; - version = "0.1.33"; + version = "0.1.34"; edition = "2018"; - sha256 = "170gc7cxyjx824r9kr17zc9gvzx89ypqfdzq259pr56gg5bwjwp6"; + sha256 = "0y3nc4mpnr79rzkrcylv5f5bnjjp19lsxwis9l4kzs97ya0jbldr"; libName = "tracing_core"; authors = [ "Tokio Contributors " @@ -10828,19 +10788,18 @@ rec { ]; }; - "wasi 0.11.0+wasi-snapshot-preview1" = rec { + "wasi 0.11.1+wasi-snapshot-preview1" = rec { crateName = "wasi"; - version = "0.11.0+wasi-snapshot-preview1"; + version = "0.11.1+wasi-snapshot-preview1"; edition = "2018"; - sha256 = "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw"; + sha256 = "0jx49r7nbkbhyfrfyhz0bm4817yrnxgd3jiwwwfv0zl439jyrwyc"; authors = [ "The Cranelift Project Developers" ]; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "default" = [ "std" ]; - "rustc-dep-of-std" = [ "compiler_builtins" "core" "rustc-std-workspace-alloc" ]; + "rustc-dep-of-std" = [ "core" "rustc-std-workspace-alloc" ]; "rustc-std-workspace-alloc" = [ "dep:rustc-std-workspace-alloc" ]; }; resolvedDefaultFeatures = [ "default" "std" ]; @@ -10944,7 +10903,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } { @@ -11045,7 +11004,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "visit" "visit-mut" "full" ]; } { @@ -11696,7 +11655,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -11726,7 +11685,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -12539,7 +12498,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "fold" ]; } { @@ -12607,7 +12566,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "full" ]; } ]; @@ -12656,7 +12615,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "fold" ]; } { @@ -12786,7 +12745,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.101"; + packageId = "syn 2.0.102"; features = [ "extra-traits" ]; } ]; diff --git a/Cargo.toml b/Cargo.toml index 78861f7..8f9ee56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", built = { version = "0.8.0", features = ["chrono", "git2"] } clap = "4.5" -const_format = "0.2" futures = { version = "0.3", features = ["compat"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index bf5a6cf..d6cc505 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -13,7 +13,6 @@ build = "build.rs" stackable-operator.workspace = true clap.workspace = true -const_format.workspace = true futures.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 93e8c0f..87b5db8 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -2,7 +2,6 @@ use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc}; use apply::apply; use build::Builder; -use const_format::concatcp; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, @@ -18,14 +17,13 @@ use update_status::update_status; use validate::validate; use crate::{ - OPERATOR_NAME, crd::{ OpenSearchConfigFragment, v1alpha1::{self}, }, framework::{ AppName, AppVersion, ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, - IsLabelValue, OperatorName, RoleGroupName, RoleName, + IsLabelValue, OperatorName, RoleGroupName, }, }; @@ -34,12 +32,35 @@ mod build; mod update_status; mod validate; -const CONTROLLER_NAME: &str = "opensearchcluster"; -pub const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, '.', OPERATOR_NAME); -const APP_NAME: &str = "opensearch"; +pub struct ContextNames { + pub app_name: AppName, + pub operator_name: OperatorName, + pub controller_name: ControllerName, +} + +pub struct Context { + client: stackable_operator::client::Client, + names: ContextNames, +} + +impl Context { + pub fn new(client: stackable_operator::client::Client, operator_name: OperatorName) -> Self { + Context { + client, + names: ContextNames { + app_name: AppName::from_str("opensearch").unwrap(), + operator_name, + controller_name: ControllerName::from_str("opensearchcluster").unwrap(), + }, + } + } -pub struct Ctx { - pub client: stackable_operator::client::Client, + pub fn full_controller_name(&self) -> String { + format!( + "{}.{}", + self.names.controller_name, self.names.operator_name + ) + } } #[derive(Snafu, Debug, EnumDiscriminants)] @@ -142,9 +163,9 @@ impl Resource for ValidatedCluster { } pub fn error_policy( - _obj: Arc>, + _object: Arc>, error: &Error, - _ctx: Arc, + _context: Arc, ) -> Action { match error { // root object is invalid, will be requed when modified @@ -155,7 +176,7 @@ pub fn error_policy( pub async fn reconcile( object: Arc>, - ctx: Arc, + context: Arc, ) -> Result { tracing::info!("Starting reconcile"); @@ -166,28 +187,19 @@ pub async fn reconcile( .map_err(Box::new) .context(InvalidOpenSearchClusterSnafu)?; - let client = &ctx.client; - // ~resolve~ dereference (client required) // validate (no client required) let validated_cluster = validate(cluster).unwrap(); // build (no client required; infallible) - let prepared_resources = Builder::new(validated_cluster.clone()).build(); + let prepared_resources = Builder::new(&context.names, validated_cluster.clone()).build(); // apply (client required) - // - // into controller context! - let app_name = AppName::from_str(APP_NAME).unwrap(); - let operator_name = OperatorName::from_str(OPERATOR_NAME).unwrap(); - let controller_name = ControllerName::from_str(CONTROLLER_NAME).unwrap(); let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); let applied_resources = apply( - client, - &app_name, - &operator_name, - &controller_name, + &context.client, + &context.names, &validated_cluster, apply_strategy, prepared_resources, @@ -196,7 +208,7 @@ pub async fn reconcile( .context(ApplyResourcesSnafu)?; // update status (client required) - update_status(client, cluster, applied_resources) + update_status(&context.client, &context.names, cluster, applied_resources) .await .context(UpdateStatusSnafu)?; diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 0104c49..7d563d9 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -2,7 +2,7 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{client::Client, cluster_resources::ClusterResourceApplyStrategy}; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{Applied, Prepared, Resources}; +use super::{Applied, Context, ContextNames, Prepared, Resources}; use crate::framework::{ AppName, ControllerName, HasNamespace, HasObjectName, HasUid, OperatorName, cluster_resources::cluster_resources_new, @@ -26,19 +26,15 @@ type Result = std::result::Result; pub async fn apply( client: &Client, - // app_name, operator_name, ... in context? - app_name: &AppName, - operator_name: &OperatorName, - controller_name: &ControllerName, + names: &ContextNames, cluster: &(impl HasObjectName + HasNamespace + HasUid), - // ref? apply_strategy: ClusterResourceApplyStrategy, resources: Resources, ) -> Result> { let mut cluster_resources = cluster_resources_new( - app_name, - operator_name, - controller_name, + &names.app_name, + &names.operator_name, + &names.controller_name, cluster, apply_strategy, ); diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 7608024..6a3c24f 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -15,35 +15,24 @@ use stackable_operator::{ kvp::Labels, }; -use super::{ - APP_NAME, CONTROLLER_NAME, Prepared, Resources, RoleGroupConfig, RoleGroupName, - ValidatedCluster, -}; -use crate::{ - OPERATOR_NAME, - framework::{ - AppName, ControllerName, OperatorName, RoleName, - kvp::label::{recommended_labels, role_group_selector}, - to_qualified_role_group_name, - }, +use super::{ContextNames, Prepared, Resources, RoleGroupConfig, RoleGroupName, ValidatedCluster}; +use crate::framework::{ + RoleName, + kvp::label::{recommended_labels, role_group_selector}, + to_qualified_role_group_name, }; -pub struct Builder { - app_name: AppName, - operator_name: OperatorName, - controller_name: ControllerName, +pub struct Builder<'a> { + names: &'a ContextNames, role_name: RoleName, cluster: ValidatedCluster, } -impl Builder { - pub fn new(cluster: ValidatedCluster) -> Builder { +impl<'a> Builder<'a> { + pub fn new(names: &'a ContextNames, cluster: ValidatedCluster) -> Builder<'a> { Builder { - // into controller context! - app_name: AppName::from_str(APP_NAME).unwrap(), + names, role_name: RoleName::from_str("nodes").unwrap(), - operator_name: OperatorName::from_str(OPERATOR_NAME).unwrap(), - controller_name: ControllerName::from_str(CONTROLLER_NAME).unwrap(), cluster, } } @@ -77,7 +66,7 @@ impl Builder { let statefulset_match_labels = role_group_selector( &self.cluster, - &self.app_name, + &self.names.app_name, &self.role_name, role_group_name, ); @@ -135,10 +124,10 @@ impl Builder { fn build_recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { recommended_labels( &self.cluster, - &self.app_name, + &self.names.app_name, &self.cluster.product_version, - &self.operator_name, - &self.controller_name, + &self.names.operator_name, + &self.names.controller_name, &self.role_name, role_group_name, ) diff --git a/rust/operator-binary/src/controller/update_status.rs b/rust/operator-binary/src/controller/update_status.rs index 2984a60..5c96b1d 100644 --- a/rust/operator-binary/src/controller/update_status.rs +++ b/rust/operator-binary/src/controller/update_status.rs @@ -8,7 +8,7 @@ use stackable_operator::{ }; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{Applied, Resources}; +use super::{Applied, ContextNames, Resources}; use crate::{ OPERATOR_NAME, crd::v1alpha1::{self, OpenSearchClusterStatus}, @@ -37,6 +37,7 @@ type Result = std::result::Result; pub async fn update_status( client: &Client, + names: &ContextNames, cluster: &v1alpha1::OpenSearchCluster, applied_resources: Resources, ) -> Result<()> { @@ -60,7 +61,7 @@ pub async fn update_status( }; client - .apply_patch_status(OPERATOR_NAME, cluster, &status) + .apply_patch_status(&format!("{}", names.operator_name), cluster, &status) .await .context(UpdateStatusSnafu)?; diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 871aed7..730a607 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,8 +1,8 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use clap::Parser as _; -use controller::FULL_CONTROLLER_NAME; use crd::{OpenSearchCluster, v1alpha1}; +use framework::OperatorName; use futures::StreamExt; use snafu::{ResultExt as _, Snafu}; use stackable_operator::{ @@ -65,8 +65,6 @@ struct Opts { cmd: Command, } -const OPERATOR_NAME: &str = "opensearch.stackable.tech"; - #[tokio::main] #[snafu::report] async fn main() -> Result<()> { @@ -98,17 +96,23 @@ async fn main() -> Result<()> { description = built_info::PKG_DESCRIPTION ); + let operator_name = OperatorName::from_str("opensearch.stackable.tech") + .expect("should be a valid operator name"); + let client = stackable_operator::client::initialize_operator( - Some(OPERATOR_NAME.to_owned()), + Some(format!("{operator_name}")), &cluster_info_opts, ) .await .context(CreateClientSnafu)?; + let controller_context = controller::Context::new(client.clone(), operator_name); + let full_controller_name = controller_context.full_controller_name(); + let event_recorder = Arc::new(Recorder::new( client.as_kube_client(), Reporter { - controller: FULL_CONTROLLER_NAME.to_owned(), + controller: full_controller_name.clone(), instance: None, }, )); @@ -130,9 +134,7 @@ async fn main() -> Result<()> { .run( controller::reconcile, controller::error_policy, - Arc::new(controller::Ctx { - client: client.clone(), - }), + Arc::new(controller_context), ) .for_each_concurrent( 16, // concurrency limit @@ -140,10 +142,11 @@ async fn main() -> Result<()> { // The event_recorder needs to be shared across all invocations, so that // events are correctly aggregated let event_recorder = event_recorder.clone(); + let full_controller_name = full_controller_name.clone(); async move { report_controller_reconciled( &event_recorder, - FULL_CONTROLLER_NAME, + &full_controller_name, &result, ) .await; From cfbf7dc897f0c464c2d55bc6c22f2c3a9b248d38 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 12 Jun 2025 10:07:40 +0200 Subject: [PATCH 09/35] Improve error handling --- rust/operator-binary/src/controller.rs | 24 ++++++----- rust/operator-binary/src/controller/apply.rs | 20 +++++---- .../src/controller/update_status.rs | 5 +-- .../src/controller/validate.rs | 41 +++++++++++++++---- rust/operator-binary/src/framework.rs | 2 +- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 87b5db8..da163e3 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -48,9 +48,10 @@ impl Context { Context { client, names: ContextNames { - app_name: AppName::from_str("opensearch").unwrap(), + app_name: AppName::from_str("opensearch").expect("should be a valid product name"), operator_name, - controller_name: ControllerName::from_str("opensearchcluster").unwrap(), + controller_name: ControllerName::from_str("opensearchcluster") + .expect("should be a valid controller name"), }, } } @@ -66,12 +67,15 @@ impl Context { #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { - #[snafu(display("OpenSearchCluster object is invalid"))] - InvalidOpenSearchCluster { + #[snafu(display("failed to deserialize cluster definition"))] + DeserializeClusterDefinition { // boxed because otherwise Clippy warns about a large enum variant source: Box, }, + #[snafu(display("failed to validate cluster"))] + ValidateCluster { source: validate::Error }, + #[snafu(display("failed to apply resources"))] ApplyResources { source: apply::Error }, @@ -99,6 +103,7 @@ pub struct ValidatedCluster { pub product_version: AppVersion, pub name: ClusterName, pub namespace: String, + pub uid: String, pub role_config: GenericRoleConfig, // "validated" means that labels are valid and no ugly rolegroup name broke them pub role_group_configs: BTreeMap, @@ -118,8 +123,7 @@ impl HasNamespace for ValidatedCluster { impl HasUid for ValidatedCluster { fn to_uid(&self) -> String { - // TODO fix - self.origin.metadata.uid.clone().unwrap() + self.uid.clone() } } @@ -169,7 +173,7 @@ pub fn error_policy( ) -> Action { match error { // root object is invalid, will be requed when modified - Error::InvalidOpenSearchCluster { .. } => Action::await_change(), + Error::DeserializeClusterDefinition { .. } => Action::await_change(), _ => Action::requeue(*Duration::from_secs(5)), } } @@ -185,12 +189,12 @@ pub async fn reconcile( .as_ref() .map_err(stackable_operator::kube::core::error_boundary::InvalidObject::clone) .map_err(Box::new) - .context(InvalidOpenSearchClusterSnafu)?; + .context(DeserializeClusterDefinitionSnafu)?; - // ~resolve~ dereference (client required) + // dereference (client required) // validate (no client required) - let validated_cluster = validate(cluster).unwrap(); + let validated_cluster = validate(cluster).context(ValidateClusterSnafu)?; // build (no client required; infallible) let prepared_resources = Builder::new(&context.names, validated_cluster.clone()).build(); diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 7d563d9..1b7c453 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -2,23 +2,22 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{client::Client, cluster_resources::ClusterResourceApplyStrategy}; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{Applied, Context, ContextNames, Prepared, Resources}; +use super::{Applied, ContextNames, Prepared, Resources}; use crate::framework::{ - AppName, ControllerName, HasNamespace, HasObjectName, HasUid, OperatorName, - cluster_resources::cluster_resources_new, + HasNamespace, HasObjectName, HasUid, cluster_resources::cluster_resources_new, }; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { - #[snafu(display("failed to delete orphaned resources"))] - DeleteOrphanedResources { + #[snafu(display("failed to apply resource"))] + ApplyResource { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to update status"))] - ApplyStatus { - source: stackable_operator::client::Error, + #[snafu(display("failed to delete orphaned resources"))] + DeleteOrphanedResources { + source: stackable_operator::cluster_resources::Error, }, } @@ -41,7 +40,10 @@ pub async fn apply( let mut applied_resources = Resources::new(); for stateful_set in resources.stateful_sets { - let applied_stateful_set = cluster_resources.add(client, stateful_set).await.unwrap(); + let applied_stateful_set = cluster_resources + .add(client, stateful_set) + .await + .context(ApplyResourceSnafu)?; applied_resources.stateful_sets.push(applied_stateful_set); } diff --git a/rust/operator-binary/src/controller/update_status.rs b/rust/operator-binary/src/controller/update_status.rs index 5c96b1d..85a4aeb 100644 --- a/rust/operator-binary/src/controller/update_status.rs +++ b/rust/operator-binary/src/controller/update_status.rs @@ -9,10 +9,7 @@ use stackable_operator::{ use strum::{EnumDiscriminants, IntoStaticStr}; use super::{Applied, ContextNames, Resources}; -use crate::{ - OPERATOR_NAME, - crd::v1alpha1::{self, OpenSearchClusterStatus}, -}; +use crate::crd::v1alpha1::{self, OpenSearchClusterStatus}; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 01f570e..577b3df 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, str::FromStr}; -use snafu::{ResultExt, Snafu}; -use stackable_operator::kube::ResourceExt; +use snafu::{OptionExt, ResultExt, Snafu}; +use stackable_operator::kube::{Resource, ResourceExt}; use strum::{EnumDiscriminants, IntoStaticStr}; use super::{AppVersion, RoleGroupName, ValidatedCluster}; @@ -10,8 +10,24 @@ use crate::{crd::v1alpha1, framework::ClusterName}; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { - #[snafu(display("failed to set recommended labels"))] - InvalidClusterName { source: crate::framework::Error }, + // TODO Improve message + #[snafu(display("failed to get the cluster name"))] + GetClusterName {}, + + #[snafu(display("failed to get the cluster namespace"))] + GetClusterNamespace {}, + + #[snafu(display("failed to get the cluster UID"))] + GetClusterUid {}, + + #[snafu(display("failed to set cluster name"))] + ParseClusterName { source: crate::framework::Error }, + + #[snafu(display("failed to set product version"))] + ParseProductVersion { source: crate::framework::Error }, + + #[snafu(display("failed to set role-group name"))] + ParseRoleGroupName { source: crate::framework::Error }, #[snafu(display("failed to set recommended labels"))] RecommendedLabels { @@ -23,14 +39,20 @@ type Result = std::result::Result; // no client needed pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result { - let cluster_name = - ClusterName::from_str(&cluster.name_unchecked()).context(InvalidClusterNameSnafu)?; + let raw_cluster_name = cluster.meta().name.clone().context(GetClusterNameSnafu)?; + let cluster_name = ClusterName::from_str(&raw_cluster_name).context(ParseClusterNameSnafu)?; + + let namespace = cluster.namespace().context(GetClusterNamespaceSnafu)?; + + let uid = cluster.uid().context(GetClusterUidSnafu)?; - let product_version = AppVersion::from_str(cluster.spec.image.product_version()).expect("oops"); + let product_version = AppVersion::from_str(cluster.spec.image.product_version()) + .context(ParseProductVersionSnafu)?; let mut role_group_configs = BTreeMap::new(); for (raw_role_group_name, role_group_config) in &cluster.spec.nodes.role_groups { - let role_group_name = RoleGroupName::from_str(raw_role_group_name).unwrap(); + let role_group_name = + RoleGroupName::from_str(raw_role_group_name).context(ParseRoleGroupNameSnafu)?; role_group_configs.insert(role_group_name, role_group_config.clone()); // TODO merge configs @@ -41,7 +63,8 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result Date: Thu, 12 Jun 2025 18:15:17 +0200 Subject: [PATCH 10/35] Merge role and role-group configs; Deploy PodDisruptionBudgets --- rust/operator-binary/src/controller.rs | 22 ++--- rust/operator-binary/src/controller/apply.rs | 89 +++++++++++++------ rust/operator-binary/src/controller/build.rs | 52 +++++++++-- .../src/controller/validate.rs | 21 +++-- rust/operator-binary/src/crd/mod.rs | 2 +- rust/operator-binary/src/framework.rs | 2 + rust/operator-binary/src/framework/builder.rs | 1 + .../src/framework/builder/pdb.rs | 24 +++++ .../src/framework/role_utils.rs | 37 ++++++++ 9 files changed, 194 insertions(+), 56 deletions(-) create mode 100644 rust/operator-binary/src/framework/builder.rs create mode 100644 rust/operator-binary/src/framework/builder/pdb.rs create mode 100644 rust/operator-binary/src/framework/role_utils.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index da163e3..f0fdc5f 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,12 +1,12 @@ use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc}; -use apply::apply; +use apply::Applier; use build::Builder; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, commons::product_image_selection::ProductImage, - k8s_openapi::api::apps::v1::StatefulSet, + k8s_openapi::api::{apps::v1::StatefulSet, policy::v1::PodDisruptionBudget}, kube::{Resource, core::DeserializeGuard, runtime::controller::Action}, logging::controller::ReconcilerError, role_utils::{GenericProductSpecificCommonConfig, GenericRoleConfig, RoleGroup}, @@ -18,7 +18,7 @@ use validate::validate; use crate::{ crd::{ - OpenSearchConfigFragment, + OpenSearchConfig, v1alpha1::{self}, }, framework::{ @@ -91,7 +91,7 @@ impl ReconcilerError for Error { } } -type RoleGroupConfig = RoleGroup; +type RoleGroupConfig = RoleGroup; // validated and converted to validated and safe types // no user errors @@ -201,13 +201,13 @@ pub async fn reconcile( // apply (client required) let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); - let applied_resources = apply( + let applied_resources = Applier::new( &context.client, &context.names, &validated_cluster, apply_strategy, - prepared_resources, ) + .apply(prepared_resources) .await .context(ApplyResourcesSnafu)?; @@ -225,14 +225,6 @@ struct Applied; struct Resources { stateful_sets: Vec, + pod_disruption_budgets: Vec, status: PhantomData, } - -impl Resources { - fn new() -> Self { - Resources { - stateful_sets: vec![], - status: PhantomData, - } - } -} diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 1b7c453..2bd68e8 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -1,5 +1,10 @@ +use std::marker::PhantomData; + use snafu::{ResultExt, Snafu}; -use stackable_operator::{client::Client, cluster_resources::ClusterResourceApplyStrategy}; +use stackable_operator::{ + client::Client, + cluster_resources::{ClusterResource, ClusterResourceApplyStrategy, ClusterResources}, +}; use strum::{EnumDiscriminants, IntoStaticStr}; use super::{Applied, ContextNames, Prepared, Resources}; @@ -23,34 +28,64 @@ pub enum Error { type Result = std::result::Result; -pub async fn apply( - client: &Client, - names: &ContextNames, - cluster: &(impl HasObjectName + HasNamespace + HasUid), - apply_strategy: ClusterResourceApplyStrategy, - resources: Resources, -) -> Result> { - let mut cluster_resources = cluster_resources_new( - &names.app_name, - &names.operator_name, - &names.controller_name, - cluster, - apply_strategy, - ); - - let mut applied_resources = Resources::new(); - for stateful_set in resources.stateful_sets { - let applied_stateful_set = cluster_resources - .add(client, stateful_set) +pub struct Applier<'a> { + client: &'a Client, + cluster_resources: ClusterResources, +} + +impl<'a> Applier<'a> { + pub fn new( + client: &'a Client, + names: &ContextNames, + cluster: &(impl HasObjectName + HasNamespace + HasUid), + apply_strategy: ClusterResourceApplyStrategy, + ) -> Applier<'a> { + let cluster_resources = cluster_resources_new( + &names.app_name, + &names.operator_name, + &names.controller_name, + cluster, + apply_strategy, + ); + + Applier { + client, + cluster_resources, + } + } + + pub async fn apply(mut self, resources: Resources) -> Result> { + let stateful_sets = self.add_resources(resources.stateful_sets).await?; + + let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?; + + self.cluster_resources + .delete_orphaned_resources(self.client) .await - .context(ApplyResourceSnafu)?; - applied_resources.stateful_sets.push(applied_stateful_set); + .context(DeleteOrphanedResourcesSnafu)?; + + Ok(Resources { + stateful_sets, + pod_disruption_budgets, + status: PhantomData, + }) } - cluster_resources - .delete_orphaned_resources(client) - .await - .context(DeleteOrphanedResourcesSnafu)?; + async fn add_resources( + &mut self, + resources: Vec, + ) -> Result> { + let mut applied_resources = vec![]; + + for resource in resources { + let applied_resource = self + .cluster_resources + .add(self.client, resource) + .await + .context(ApplyResourceSnafu)?; + applied_resources.push(applied_resource); + } - Ok(applied_resources) + Ok(applied_resources) + } } diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 6a3c24f..54bef74 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{marker::PhantomData, str::FromStr}; use stackable_operator::{ builder::{ @@ -9,6 +9,7 @@ use stackable_operator::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{Container, PodTemplateSpec}, + policy::v1::PodDisruptionBudget, }, apimachinery::pkg::apis::meta::v1::LabelSelector, }, @@ -18,10 +19,13 @@ use stackable_operator::{ use super::{ContextNames, Prepared, Resources, RoleGroupConfig, RoleGroupName, ValidatedCluster}; use crate::framework::{ RoleName, + builder::pdb::pod_disruption_budget_builder_with_role, kvp::label::{recommended_labels, role_group_selector}, to_qualified_role_group_name, }; +const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; + pub struct Builder<'a> { names: &'a ContextNames, role_name: RoleName, @@ -38,13 +42,22 @@ impl<'a> Builder<'a> { } pub fn build(&self) -> Resources { - let mut resources = Resources::new(); - for (role_group_name, role_group_config) in self.cluster.role_group_configs.iter() { - resources - .stateful_sets - .push(self.build_statefulset(role_group_name, role_group_config)); + let stateful_sets = self + .cluster + .role_group_configs + .iter() + .map(|(role_group_name, role_group_config)| { + self.build_statefulset(role_group_name, role_group_config) + }) + .collect(); + + let pod_disruption_budgets = self.build_pdb().into_iter().collect(); + + Resources { + stateful_sets, + pod_disruption_budgets, + status: PhantomData, } - resources } fn build_statefulset( @@ -84,6 +97,8 @@ impl<'a> Builder<'a> { ..StatefulSetSpec::default() }; + // TODO Implement overrides + StatefulSet { metadata, spec: Some(spec), @@ -132,4 +147,27 @@ impl<'a> Builder<'a> { role_group_name, ) } + + fn build_pdb(&self) -> Option { + let pdb_config = &self.cluster.role_config.pod_disruption_budget; + + if pdb_config.enabled { + let max_unavailable = pdb_config + .max_unavailable + .unwrap_or(PDB_DEFAULT_MAX_UNAVAILABLE); + Some( + pod_disruption_budget_builder_with_role( + &self.cluster, + &self.names.app_name, + &self.role_name, + &self.names.operator_name, + &self.names.controller_name, + ) + .with_max_unavailable(max_unavailable) + .build(), + ) + } else { + None + } + } } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 577b3df..4f4fb69 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -5,7 +5,10 @@ use stackable_operator::kube::{Resource, ResourceExt}; use strum::{EnumDiscriminants, IntoStaticStr}; use super::{AppVersion, RoleGroupName, ValidatedCluster}; -use crate::{crd::v1alpha1, framework::ClusterName}; +use crate::{ + crd::{OpenSearchConfigFragment, v1alpha1}, + framework::{ClusterName, role_utils::with_validated_config}, +}; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] @@ -29,9 +32,9 @@ pub enum Error { #[snafu(display("failed to set role-group name"))] ParseRoleGroupName { source: crate::framework::Error }, - #[snafu(display("failed to set recommended labels"))] - RecommendedLabels { - source: stackable_operator::kvp::LabelError, + #[snafu(display("fragment validation failure"))] + ValidateOpenSearchConfig { + source: stackable_operator::config::fragment::ValidationError, }, } @@ -53,9 +56,15 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result + IsLabelValue), + app_name: &AppName, + role_name: &RoleName, + operator_name: &OperatorName, + controller_name: &ControllerName, +) -> PodDisruptionBudgetBuilder { + PodDisruptionBudgetBuilder::new_with_role( + owner, + &app_name.to_label_value(), + &role_name.to_label_value(), + &operator_name.to_label_value(), + &controller_name.to_label_value(), + ) + .expect("Labels should be created because all given parameters produce valid label values") +} diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs new file mode 100644 index 0000000..d21cd45 --- /dev/null +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -0,0 +1,37 @@ +use serde::Serialize; +use stackable_operator::{ + config::{ + fragment::{self, FromFragment}, + merge::Merge, + }, + role_utils::{CommonConfiguration, Role, RoleGroup}, + schemars::JsonSchema, +}; + +pub fn with_validated_config( + role_group: &RoleGroup, + role: &Role, + default_config: &T, +) -> Result, fragment::ValidationError> +where + C: FromFragment, + ProductSpecificCommonConfig: Clone, + T: Merge + Clone, + U: Default + JsonSchema + Serialize, +{ + let validated_config = role_group.validate_config(role, default_config)?; + Ok(RoleGroup { + config: CommonConfiguration { + config: validated_config, + config_overrides: role_group.config.config_overrides.clone(), + env_overrides: role_group.config.env_overrides.clone(), + cli_overrides: role_group.config.cli_overrides.clone(), + pod_overrides: role_group.config.pod_overrides.clone(), + product_specific_common_config: role_group + .config + .product_specific_common_config + .clone(), + }, + replicas: role_group.replicas, + }) +} From 66cb44ce5e09ef22d592e7c45098550b789ecfd4 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 13 Jun 2025 18:07:09 +0200 Subject: [PATCH 11/35] Improve macro attributed_string_type --- rust/operator-binary/src/framework.rs | 117 +++++++++++++++++--------- 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 755ea32..b7d752a 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -51,69 +51,108 @@ pub trait IsLabelValue { } /// max_length must not exceed 63! This cannot be checked at compile time. -macro_rules! object_name { - ($name:ident, $max_length:literal) => { +macro_rules! attributed_string_type { + ($name:ident $(, $attribute:tt)*) => { /// Bla #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct $name(String); - impl $name { - // type arithmetic would be better - pub const MAX_LENGTH: usize = $max_length; - } - impl Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } + impl FromStr for $name { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + + $(attributed_string_type!(@from_str $name, s, $attribute);)* + + Ok(Self(s.to_owned())) + } + } + + $(attributed_string_type!(@trait_impl $name, $attribute);)* + }; + (@from_str $name:ident, $s:expr, (max_length = $max_length:literal)) => { + let length = $s.len() as usize; + ensure!( + length <= $name::MAX_LENGTH, + LengthExceededSnafu { + length, + max_length: $name::MAX_LENGTH, + } + ); + }; + (@from_str $name:ident, $s:expr, is_object_name) => { + stackable_operator::validation::is_rfc_1123_subdomain($s).context(InvalidObjectNameSnafu)?; + }; + (@from_str $name:ident, $s:expr, is_valid_label_value) => { + LabelValue::from_str($s).context(InvalidLabelValueSnafu)?; + }; + (@trait_impl $name:ident, (max_length = $max_length:literal)) => { + impl $name { + // type arithmetic would be better + pub const MAX_LENGTH: usize = $max_length; + } + }; + (@trait_impl $name:ident, is_object_name) => { impl HasObjectName for $name { fn to_object_name(&self) -> String { self.0.clone() } } - + }; + (@trait_impl $name:ident, is_valid_label_value) => { impl IsLabelValue for $name { fn to_label_value(&self) -> String { self.0.clone() } } - - impl FromStr for $name { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - let length = s.len() as usize; - ensure!( - /* length <= LABEL_VALUE_MAX_LENGTH && */ length <= $name::MAX_LENGTH, - LengthExceededSnafu { - length, - max_length: $name::MAX_LENGTH, - } - ); - - stackable_operator::validation::is_rfc_1123_subdomain(s) - .context(InvalidObjectNameSnafu)?; - - LabelValue::from_str(s).context(InvalidLabelValueSnafu)?; - - Ok(Self(s.to_owned())) - } - } }; } // There are compile time checks elsewhere ... -// TODO Do not automatically derive ToObjectName and ToLabelValue. -// Version is no object name! -object_name!(AppName, 54); -object_name!(AppVersion, 63); -object_name!(ClusterName, 63); -object_name!(ControllerName, 63); -object_name!(OperatorName, 63); -object_name!(RoleGroupName, 63); -object_name!(RoleName, 63); +attributed_string_type! { + AppName, + (max_length = 54), + is_valid_label_value +} +attributed_string_type! { + AppVersion, + (max_length = 63), + is_valid_label_value +} +attributed_string_type! { + ClusterName, + (max_length = 63), + is_object_name, + is_valid_label_value +} +attributed_string_type! { + ControllerName, + (max_length = 63), + is_valid_label_value +} +attributed_string_type! { + OperatorName, + (max_length = 63), + is_valid_label_value +} +attributed_string_type! { + RoleGroupName, + (max_length = 63), + is_object_name, + is_valid_label_value +} +attributed_string_type! { + RoleName, + (max_length = 63), + is_object_name, + is_valid_label_value +} pub fn to_qualified_role_group_name( cluster_name: &ClusterName, From 15652c5052e6e75025380a2fb06931260a81850b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 17 Jun 2025 09:26:43 +0200 Subject: [PATCH 12/35] Rename AppName and AppVersion to ProductName and ProductVersion --- rust/operator-binary/build.rs | 2 +- rust/operator-binary/src/controller.rs | 11 +-- rust/operator-binary/src/controller/apply.rs | 2 +- rust/operator-binary/src/controller/build.rs | 11 ++- .../src/controller/validate.rs | 4 +- rust/operator-binary/src/framework.rs | 73 ++++++++++++------- .../src/framework/builder/pdb.rs | 6 +- .../src/framework/cluster_resources.rs | 12 +-- .../src/framework/kvp/label.rs | 15 ++-- 9 files changed, 80 insertions(+), 56 deletions(-) diff --git a/rust/operator-binary/build.rs b/rust/operator-binary/build.rs index fa809bf..7ca1c47 100644 --- a/rust/operator-binary/build.rs +++ b/rust/operator-binary/build.rs @@ -1,3 +1,3 @@ fn main() { - built::write_built_file().unwrap(); + built::write_built_file().expect("Built file should have been written."); } diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index f0fdc5f..997eb38 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -22,8 +22,8 @@ use crate::{ v1alpha1::{self}, }, framework::{ - AppName, AppVersion, ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, - IsLabelValue, OperatorName, RoleGroupName, + ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, + OperatorName, ProductName, ProductVersion, RoleGroupName, }, }; @@ -33,7 +33,7 @@ mod update_status; mod validate; pub struct ContextNames { - pub app_name: AppName, + pub product_name: ProductName, pub operator_name: OperatorName, pub controller_name: ControllerName, } @@ -48,7 +48,8 @@ impl Context { Context { client, names: ContextNames { - app_name: AppName::from_str("opensearch").expect("should be a valid product name"), + product_name: ProductName::from_str("opensearch") + .expect("should be a valid product name"), operator_name, controller_name: ControllerName::from_str("opensearchcluster") .expect("should be a valid controller name"), @@ -100,7 +101,7 @@ type RoleGroupConfig = RoleGroup Applier<'a> { apply_strategy: ClusterResourceApplyStrategy, ) -> Applier<'a> { let cluster_resources = cluster_resources_new( - &names.app_name, + &names.product_name, &names.operator_name, &names.controller_name, cluster, diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 54bef74..7835c66 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -36,7 +36,7 @@ impl<'a> Builder<'a> { pub fn new(names: &'a ContextNames, cluster: ValidatedCluster) -> Builder<'a> { Builder { names, - role_name: RoleName::from_str("nodes").unwrap(), + role_name: RoleName::from_str("nodes").expect("should be a valid role name"), cluster, } } @@ -79,7 +79,7 @@ impl<'a> Builder<'a> { let statefulset_match_labels = role_group_selector( &self.cluster, - &self.names.app_name, + &self.names.product_name, &self.role_name, role_group_name, ); @@ -127,9 +127,8 @@ impl<'a> Builder<'a> { .image .resolve("opensearch", crate::built_info::PKG_VERSION); - // TODO ContainerName as typed string? ContainerBuilder::new("opensearch") - .expect("ContainerBuilder not created") + .expect("should be a valid container name") .image_from_product_image(&product_image) .add_env_var("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "super@Secret1") .add_env_var("cluster.initial_master_nodes", "opensearch-0") @@ -139,7 +138,7 @@ impl<'a> Builder<'a> { fn build_recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { recommended_labels( &self.cluster, - &self.names.app_name, + &self.names.product_name, &self.cluster.product_version, &self.names.operator_name, &self.names.controller_name, @@ -158,7 +157,7 @@ impl<'a> Builder<'a> { Some( pod_disruption_budget_builder_with_role( &self.cluster, - &self.names.app_name, + &self.names.product_name, &self.role_name, &self.names.operator_name, &self.names.controller_name, diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 4f4fb69..bdf2192 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -4,7 +4,7 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::kube::{Resource, ResourceExt}; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{AppVersion, RoleGroupName, ValidatedCluster}; +use super::{ProductVersion, RoleGroupName, ValidatedCluster}; use crate::{ crd::{OpenSearchConfigFragment, v1alpha1}, framework::{ClusterName, role_utils::with_validated_config}, @@ -49,7 +49,7 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result String; } +/// Has a namespace pub trait HasNamespace { fn to_namespace(&self) -> String; } +/// Has a Kubernetes UID pub trait HasUid { fn to_uid(&self) -> String; } +/// Is a valid label value as defined in RFC 1123. pub trait IsLabelValue { fn to_label_value(&self) -> String; } -/// max_length must not exceed 63! This cannot be checked at compile time. +/// Restricted string type with attributes like maximum length. macro_rules! attributed_string_type { - ($name:ident $(, $attribute:tt)*) => { - /// Bla + ($name:ident, $description:literal $(, $attribute:tt)*) => { + #[doc = $description] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct $name(String); @@ -76,7 +81,7 @@ macro_rules! attributed_string_type { $(attributed_string_type!(@trait_impl $name, $attribute);)* }; - (@from_str $name:ident, $s:expr, (max_length = $max_length:literal)) => { + (@from_str $name:ident, $s:expr, (max_length = $max_length:expr)) => { let length = $s.len() as usize; ensure!( length <= $name::MAX_LENGTH, @@ -92,7 +97,7 @@ macro_rules! attributed_string_type { (@from_str $name:ident, $s:expr, is_valid_label_value) => { LabelValue::from_str($s).context(InvalidLabelValueSnafu)?; }; - (@trait_impl $name:ident, (max_length = $max_length:literal)) => { + (@trait_impl $name:ident, (max_length = $max_length:expr)) => { impl $name { // type arithmetic would be better pub const MAX_LENGTH: usize = $max_length; @@ -114,55 +119,73 @@ macro_rules! attributed_string_type { }; } -// There are compile time checks elsewhere ... attributed_string_type! { - AppName, + ProductName, + "The name of a product, e.g. \"opensearch\"", + // A suffix is added to produce a label value. An according compile-time check ensures that + // max_length cannot be set higher. (max_length = 54), is_valid_label_value } attributed_string_type! { - AppVersion, - (max_length = 63), + ProductVersion, + "The version of a product, e.g. \"3.0.0\"", + (max_length = LABEL_VALUE_MAX_LENGTH), is_valid_label_value } attributed_string_type! { ClusterName, - (max_length = 63), + "The name of a cluster/stacklet, e.g. \"my-opensearch-cluster\"", + (max_length = LABEL_VALUE_MAX_LENGTH), is_object_name, is_valid_label_value } attributed_string_type! { ControllerName, - (max_length = 63), + "The name of a controller in an operator, e.g. \"opensearchcluster\"", + (max_length = LABEL_VALUE_MAX_LENGTH), is_valid_label_value } attributed_string_type! { OperatorName, - (max_length = 63), + "The name of an operator, e.g. \"opensearch.stackable.tech\"", + (max_length = LABEL_VALUE_MAX_LENGTH), is_valid_label_value } attributed_string_type! { RoleGroupName, - (max_length = 63), + "The name of a role-group name, e.g. \"clusterManager\"", + (max_length = LABEL_VALUE_MAX_LENGTH), is_object_name, is_valid_label_value } attributed_string_type! { RoleName, - (max_length = 63), + "The name of a role name, e.g. \"nodes\"", + (max_length = LABEL_VALUE_MAX_LENGTH), is_object_name, is_valid_label_value } +/// Creates a qualified role group name consisting of the cluster name, role name and role-group +/// name. +/// The result is a valid DNS subdomain name as defined in RFC 1123 that can be used e.g. as a name +/// for a StatefulSet. pub fn to_qualified_role_group_name( cluster_name: &ClusterName, role_name: &RoleName, role_group_name: &RoleGroupName, ) -> String { - // Compile time check + // Compile-time check const _: () = assert!( - ClusterName::MAX_LENGTH + RoleName::MAX_LENGTH + RoleGroupName::MAX_LENGTH - <= _OBJECT_NAME_MAX_LENGTH - 3 /* dashes */ - 4, /* digits */ + ClusterName::MAX_LENGTH + + 1 /* dash */ + + RoleName::MAX_LENGTH + + 1 /* dash */ + + RoleGroupName::MAX_LENGTH + + 1 /* dash */ + + 4 /* digits */ + <= OBJECT_NAME_MAX_LENGTH, "The maximum lengths of the cluster name, role name and role group name must be defined so that the combination of these names (including separators and the sequential pod number) is also a valid object name with a maximum of 263 characters (see RFC 1123)" ); @@ -179,14 +202,14 @@ mod tests { use std::str::FromStr; use super::{ClusterName, RoleGroupName, RoleName, to_qualified_role_group_name}; - use crate::framework::AppName; + use crate::framework::ProductName; #[test] fn test_object_name_constraints() { - assert!(AppName::from_str("valid-role-group-name").is_ok()); - assert!(AppName::from_str("invalid-character: /").is_err()); + assert!(ProductName::from_str("valid-role-group-name").is_ok()); + assert!(ProductName::from_str("invalid-character: /").is_err()); assert!( - AppName::from_str( + ProductName::from_str( "too-long-123456789012345678901234567890123456789012345678901234567890" ) .is_err() diff --git a/rust/operator-binary/src/framework/builder/pdb.rs b/rust/operator-binary/src/framework/builder/pdb.rs index 825b7d2..0f3be90 100644 --- a/rust/operator-binary/src/framework/builder/pdb.rs +++ b/rust/operator-binary/src/framework/builder/pdb.rs @@ -4,18 +4,18 @@ use stackable_operator::{ kube::{Resource, api::ObjectMeta}, }; -use crate::framework::{AppName, ControllerName, IsLabelValue, OperatorName, RoleName}; +use crate::framework::{ControllerName, IsLabelValue, OperatorName, ProductName, RoleName}; pub fn pod_disruption_budget_builder_with_role( owner: &(impl Resource + IsLabelValue), - app_name: &AppName, + product_name: &ProductName, role_name: &RoleName, operator_name: &OperatorName, controller_name: &ControllerName, ) -> PodDisruptionBudgetBuilder { PodDisruptionBudgetBuilder::new_with_role( owner, - &app_name.to_label_value(), + &product_name.to_label_value(), &role_name.to_label_value(), &operator_name.to_label_value(), &controller_name.to_label_value(), diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs index 5560780..ffdfd12 100644 --- a/rust/operator-binary/src/framework/cluster_resources.rs +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -4,12 +4,12 @@ use stackable_operator::{ }; use super::{ - AppName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, + ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, ProductName, }; use crate::framework::kvp::label::LABEL_VALUE_MAX_LENGTH; pub fn cluster_resources_new( - app_name: &AppName, + product_name: &ProductName, operator_name: &OperatorName, controller_name: &ControllerName, cluster: &(impl HasObjectName + HasNamespace + HasUid), @@ -17,14 +17,14 @@ pub fn cluster_resources_new( ) -> ClusterResources { // ClusterResources::new creates a label value from the given app name by appending // `-operator`. For the resulting label value to be valid, it must not exceed 63 characters. - // Check at compile time that AppName::MAX_LENGTH is defined accordingly. + // Check at compile time that ProductName::MAX_LENGTH is defined accordingly. const _: () = assert!( - AppName::MAX_LENGTH <= LABEL_VALUE_MAX_LENGTH - "-operator".len(), - "The label value `-operator` must not exceed 63 characters." + ProductName::MAX_LENGTH <= LABEL_VALUE_MAX_LENGTH - "-operator".len(), + "The label value `-operator` must not exceed 63 characters." ); ClusterResources::new( - &app_name.to_label_value(), + &product_name.to_label_value(), &operator_name.to_label_value(), &controller_name.to_label_value(), &ObjectReference { diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs index ebaeb8d..48b79af 100644 --- a/rust/operator-binary/src/framework/kvp/label.rs +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -4,15 +4,16 @@ use stackable_operator::{ }; use crate::framework::{ - AppName, AppVersion, ControllerName, IsLabelValue, OperatorName, RoleGroupName, RoleName, + ControllerName, IsLabelValue, OperatorName, ProductName, ProductVersion, RoleGroupName, + RoleName, }; pub const LABEL_VALUE_MAX_LENGTH: usize = 63; pub fn recommended_labels( owner: &(impl Resource + IsLabelValue), - app_name: &AppName, - app_version: &AppVersion, + product_name: &ProductName, + product_version: &ProductVersion, operator_name: &OperatorName, controller_name: &ControllerName, role_name: &RoleName, @@ -20,8 +21,8 @@ pub fn recommended_labels( ) -> Labels { let object_labels = ObjectLabels { owner, - app_name: &app_name.to_label_value(), - app_version: &app_version.to_label_value(), + app_name: &product_name.to_label_value(), + app_version: &product_version.to_label_value(), operator_name: &operator_name.to_label_value(), controller_name: &controller_name.to_label_value(), role: &role_name.to_label_value(), @@ -33,13 +34,13 @@ pub fn recommended_labels( pub fn role_group_selector( owner: &(impl Resource + IsLabelValue), - app_name: &AppName, + product_name: &ProductName, role_name: &RoleName, role_group_name: &RoleGroupName, ) -> Labels { Labels::role_group_selector( owner, - &app_name.to_label_value(), + &product_name.to_label_value(), &role_name.to_label_value(), &role_group_name.to_label_value(), ) From 96b1164de4ae450250316088586fb04af0de93dc Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 20 Jun 2025 18:21:23 +0200 Subject: [PATCH 13/35] Add configuration options --- rust/operator-binary/src/controller.rs | 35 +++- rust/operator-binary/src/controller/apply.rs | 3 + rust/operator-binary/src/controller/build.rs | 153 ++++++++++++++++-- .../src/controller/node_config.rs | 103 ++++++++++++ .../src/controller/validate.rs | 4 +- rust/operator-binary/src/crd/mod.rs | 118 +++++++++++--- tests/release.yaml | 16 ++ tests/templates/kuttl/smoke/10-assert.yaml | 102 ++++++++++++ .../kuttl/smoke/10-install-opensearch.yaml | 14 +- tests/test-definition.yaml | 34 ++++ 10 files changed, 541 insertions(+), 41 deletions(-) create mode 100644 rust/operator-binary/src/controller/node_config.rs create mode 100644 tests/release.yaml create mode 100644 tests/templates/kuttl/smoke/10-assert.yaml create mode 100644 tests/test-definition.yaml diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 997eb38..f28eed0 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -6,7 +6,7 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, commons::product_image_selection::ProductImage, - k8s_openapi::api::{apps::v1::StatefulSet, policy::v1::PodDisruptionBudget}, + k8s_openapi::api::{apps::v1::StatefulSet, core::v1::Service, policy::v1::PodDisruptionBudget}, kube::{Resource, core::DeserializeGuard, runtime::controller::Action}, logging::controller::ReconcilerError, role_utils::{GenericProductSpecificCommonConfig, GenericRoleConfig, RoleGroup}, @@ -17,10 +17,7 @@ use update_status::update_status; use validate::validate; use crate::{ - crd::{ - OpenSearchConfig, - v1alpha1::{self}, - }, + crd::v1alpha1::{self}, framework::{ ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, ProductName, ProductVersion, RoleGroupName, @@ -29,6 +26,7 @@ use crate::{ mod apply; mod build; +mod node_config; mod update_status; mod validate; @@ -92,7 +90,7 @@ impl ReconcilerError for Error { } } -type RoleGroupConfig = RoleGroup; +type RoleGroupConfig = RoleGroup; // validated and converted to validated and safe types // no user errors @@ -110,6 +108,30 @@ pub struct ValidatedCluster { pub role_group_configs: BTreeMap, } +impl ValidatedCluster { + pub fn is_single_node(&self) -> bool { + self.node_count() == 1 + } + + pub fn node_count(&self) -> u32 { + self.role_group_configs + .values() + .map(|rg| rg.replicas.unwrap_or(1) as u32) + .sum() + } + + pub fn role_group_configs_filtered_by_node_role( + &self, + node_role: &v1alpha1::NodeRole, + ) -> BTreeMap { + self.role_group_configs + .clone() + .into_iter() + .filter(|c| c.1.config.config.node_roles.contains(node_role)) + .collect() + } +} + impl HasObjectName for ValidatedCluster { fn to_object_name(&self) -> String { self.name.to_object_name() @@ -226,6 +248,7 @@ struct Applied; struct Resources { stateful_sets: Vec, + services: Vec, pod_disruption_budgets: Vec, status: PhantomData, } diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 382cdc3..9c849c4 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -57,6 +57,8 @@ impl<'a> Applier<'a> { pub async fn apply(mut self, resources: Resources) -> Result> { let stateful_sets = self.add_resources(resources.stateful_sets).await?; + let services = self.add_resources(resources.services).await?; + let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?; self.cluster_resources @@ -66,6 +68,7 @@ impl<'a> Applier<'a> { Ok(Resources { stateful_sets, + services, pod_disruption_budgets, status: PhantomData, }) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 7835c66..7748270 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -3,20 +3,34 @@ use std::{marker::PhantomData, str::FromStr}; use stackable_operator::{ builder::{ meta::ObjectMetaBuilder, - pod::{PodBuilder, container::ContainerBuilder}, + pod::{ + PodBuilder, + container::{ContainerBuilder, FieldPathEnvVar}, + }, }, k8s_openapi::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, - core::v1::{Container, PodTemplateSpec}, + core::v1::{ + Container, ContainerPort, PodTemplateSpec, Service, ServicePort, ServiceSpec, + }, policy::v1::PodDisruptionBudget, }, apimachinery::pkg::apis::meta::v1::LabelSelector, }, - kvp::Labels, + kvp::{ + Label, Labels, + consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, + }, }; -use super::{ContextNames, Prepared, Resources, RoleGroupConfig, RoleGroupName, ValidatedCluster}; +use super::{ + ContextNames, Prepared, Resources, RoleGroupConfig, RoleGroupName, ValidatedCluster, + node_config::{ + DISCOVERY_SEED_HOSTS, DISCOVERY_TYPE, INITIAL_CLUSTER_MANAGER_NODES, NETWORK_HOST, + NODE_NAME, NODE_ROLES, NodeConfig, + }, +}; use crate::framework::{ RoleName, builder::pdb::pod_disruption_budget_builder_with_role, @@ -30,14 +44,17 @@ pub struct Builder<'a> { names: &'a ContextNames, role_name: RoleName, cluster: ValidatedCluster, + node_config: NodeConfig, } impl<'a> Builder<'a> { pub fn new(names: &'a ContextNames, cluster: ValidatedCluster) -> Builder<'a> { + let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); Builder { names, - role_name: RoleName::from_str("nodes").expect("should be a valid role name"), - cluster, + role_name: role_name.clone(), + cluster: cluster.clone(), + node_config: NodeConfig::new(role_name, cluster), } } @@ -51,10 +68,15 @@ impl<'a> Builder<'a> { }) .collect(); + let services = vec![self.build_cluster_manager_service()]; + + // TODO Create further services + let pod_disruption_budgets = self.build_pdb().into_iter().collect(); Resources { stateful_sets, + services, pod_disruption_budgets, status: PhantomData, } @@ -75,7 +97,7 @@ impl<'a> Builder<'a> { .with_labels(self.build_recommended_labels(role_group_name)) .build(); - let template = self.build_pod_template(role_group_name); + let template = self.build_pod_template(role_group_name, role_group_config); let statefulset_match_labels = role_group_selector( &self.cluster, @@ -106,14 +128,26 @@ impl<'a> Builder<'a> { } } - fn build_pod_template(&self, role_group_name: &RoleGroupName) -> PodTemplateSpec { + fn build_pod_template( + &self, + role_group_name: &RoleGroupName, + role_group_config: &RoleGroupConfig, + ) -> PodTemplateSpec { let mut builder = PodBuilder::new(); + let opensearch_config = &role_group_config.config.config; + let mut node_role_labels = Labels::new(); + for node_role in opensearch_config.node_roles.iter() { + node_role_labels + .insert(Label::try_from((format!("{node_role}"), "true".to_string())).unwrap()); + } + let metadata = ObjectMetaBuilder::new() .with_labels(self.build_recommended_labels(role_group_name)) + .with_labels(node_role_labels) .build(); - let container = self.build_container(); + let container = self.build_container(role_group_config); builder .metadata(metadata) @@ -121,17 +155,49 @@ impl<'a> Builder<'a> { .build_template() } - fn build_container(&self) -> Container { + fn build_container(&self, role_group_config: &RoleGroupConfig) -> Container { let product_image = self .cluster .image .resolve("opensearch", crate::built_info::PKG_VERSION); + let opensearch_config = &role_group_config.config.config; + ContainerBuilder::new("opensearch") .expect("should be a valid container name") .image_from_product_image(&product_image) + .add_env_var( + DISCOVERY_SEED_HOSTS, + self.node_config.discovery_seed_hosts(), + ) + .add_env_var(DISCOVERY_TYPE, self.node_config.discovery_type()) + .add_env_var( + INITIAL_CLUSTER_MANAGER_NODES, + self.node_config + .initial_cluster_manager_nodes(&opensearch_config.node_roles), + ) + .add_env_var(NETWORK_HOST, self.node_config.network_host()) + // TODO Is this option also required on a proper custom image? .add_env_var("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "super@Secret1") - .add_env_var("cluster.initial_master_nodes", "opensearch-0") + // Set the OpenSearch node name to the Pod name. + // The node name is used e.g. for `{INITIAL_CLUSTER_MANAGER_NODES}`. + .add_env_var_from_field_path(NODE_NAME, FieldPathEnvVar::Name) + .add_env_var( + NODE_ROLES, + self.node_config.node_roles(&opensearch_config.node_roles), + ) + .add_container_ports(vec![ + ContainerPort { + name: Some("http".to_owned()), + container_port: 9200, + ..ContainerPort::default() + }, + ContainerPort { + name: Some("transport".to_owned()), + container_port: 9300, + ..ContainerPort::default() + }, + ]) .build() } @@ -147,6 +213,71 @@ impl<'a> Builder<'a> { ) } + fn build_cluster_manager_service(&self) -> Service { + let ports = vec![ + ServicePort { + name: Some("http".to_owned()), + port: 9200, + ..ServicePort::default() + }, + ServicePort { + name: Some("transport".to_owned()), + port: 9300, + ..ServicePort::default() + }, + ]; + + // Well-known Kubernetes labels + let mut labels = Labels::role_selector( + &self.cluster, + &self.names.product_name.to_string(), + &self.role_name.to_string(), + ) + .unwrap(); + + let managed_by = Label::managed_by( + &self.names.operator_name.to_string(), + &self.names.controller_name.to_string(), + ) + .unwrap(); + let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); + + labels.insert(managed_by); + labels.insert(version); + + // Stackable-specific labels + labels + .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) + .unwrap(); + + let metadata = ObjectMetaBuilder::new() + .name(format!("{}-cluster-manager", self.cluster.name)) + .namespace(&self.cluster.namespace) + .ownerreference_from_resource(&self.cluster, None, Some(true)) + // TODO Fix + .unwrap() + .with_labels(labels) + .build(); + + let service_selector = [("cluster-manager".to_owned(), "true".to_owned())].into(); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(service_selector), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, + } + } + fn build_pdb(&self) -> Option { let pdb_config = &self.cluster.role_config.pod_disruption_budget; diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/node_config.rs new file mode 100644 index 0000000..f5c8594 --- /dev/null +++ b/rust/operator-binary/src/controller/node_config.rs @@ -0,0 +1,103 @@ +use super::ValidatedCluster; +use crate::{ + crd::{NodeRoles, v1alpha1}, + framework::{RoleName, to_qualified_role_group_name}, +}; + +pub const DISCOVERY_SEED_HOSTS: &str = "discovery.seed_hosts"; +pub const DISCOVERY_TYPE: &str = "discovery.type"; +pub const INITIAL_CLUSTER_MANAGER_NODES: &str = "cluster.initial_cluster_manager_nodes"; +pub const NETWORK_HOST: &str = "network.host"; +pub const NODE_NAME: &str = "node.name"; +pub const NODE_ROLES: &str = "node.roles"; + +pub struct NodeConfig { + role_name: RoleName, + cluster: ValidatedCluster, +} + +impl NodeConfig { + pub fn new(role_name: RoleName, cluster: ValidatedCluster) -> Self { + Self { role_name, cluster } + } + + pub fn discovery_seed_hosts(&self) -> String { + // TODO Fix + format!("{}-cluster-manager", self.cluster.name) + } + + /// Configuration for `{DISCOVERY_TYPE}` + /// + /// "zen" is the default if `{DISCOVERY_TYPE}` is not set. + /// It is nevertheless explicitly set here. + /// see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/discovery/DiscoveryModule.java#L88-L89 + pub fn discovery_type(&self) -> String { + if self.cluster.is_single_node() { + "single-node".to_owned() + } else { + "zen".to_owned() + } + } + + /// Configuration for `cluster.initial_cluster_manager_nodes` which replaces + /// `cluster.initial_master_nodes`, see + /// https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/coordination/ClusterBootstrapService.java#L79-L93. + /// + /// According to + /// https://docs.opensearch.org/docs/3.0/install-and-configure/configuring-opensearch/discovery-gateway-settings/, + /// it contains "a list of cluster-manager-eligible nodes used to bootstrap the cluster." + /// + /// However, the documentation for Elasticsearch is more detailed and contains the following + /// notes (see https://www.elastic.co/guide/en/elasticsearch/reference/9.0/modules-discovery-settings.html): + /// * Remove this setting once the cluster has formed, and never set it again for this cluster. + /// * Do not configure this setting on master-ineligible nodes. + /// * Do not configure this setting on nodes joining an existing cluster. + /// * Do not configure this setting on nodes which are restarting. + /// * Do not configure this setting when performing a full-cluster restart. + /// + /// The OpenSearch Helm chart only sets master nodes but does not handle the other cases (see + /// https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/templates/statefulset.yaml#L414-L415), + /// so they are also ignored here for the moment. + pub fn initial_cluster_manager_nodes(&self, node_roles: &NodeRoles) -> String { + if !self.cluster.is_single_node() + && node_roles.contains(&v1alpha1::NodeRole::ClusterManager) + { + let cluster_manager_configs = self + .cluster + .role_group_configs_filtered_by_node_role(&v1alpha1::NodeRole::ClusterManager); + + // This setting requires node names as set in `{NODE_NAME}`. + // The node names are set to the pod names with + // `valueFrom.fieldRef.fieldPath: metadata.name`, so it is okay to calculate the pod + // names here and use them as node names. + let mut pod_names = vec![]; + for (role_group_name, role_group_config) in cluster_manager_configs { + let sts_name = to_qualified_role_group_name( + &self.cluster.name, + &self.role_name, + &role_group_name, + ); + pod_names.extend( + (0..role_group_config.replicas.unwrap_or(1)).map(|i| format!("{sts_name}-{i}")), + ); + } + pod_names.join(",") + } else { + // This setting is not allowed on single node cluster, see + // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/coordination/ClusterBootstrapService.java#L126-L136 + String::new() + } + } + + pub fn network_host(&self) -> String { + "0.0.0.0".to_owned() + } + + pub fn node_roles(&self, node_roles: &NodeRoles) -> String { + node_roles + .iter() + .map(|r| format!("{}", r)) + .collect::>() + .join(",") + } +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index bdf2192..b8ffddc 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -6,7 +6,7 @@ use strum::{EnumDiscriminants, IntoStaticStr}; use super::{ProductVersion, RoleGroupName, ValidatedCluster}; use crate::{ - crd::{OpenSearchConfigFragment, v1alpha1}, + crd::v1alpha1, framework::{ClusterName, role_utils::with_validated_config}, }; @@ -60,7 +60,7 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result, } + // The possible node roles are by default the built-in roles and the search role, see + // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java#L609-L614. + // + // Plugins can set additional roles, see + // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java#L629-L646. + // + // For instance, the ml-commons plugin adds the node role "ml", see + // https://github.com/opensearch-project/ml-commons/blob/3.0.0.0/plugin/src/main/java/org/opensearch/ml/plugin/MachineLearningPlugin.java#L394. + // If such a plugin is added, then this enumeration must be extended accordingly. + #[derive( + Clone, Debug, Deserialize, Display, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + // The OpenSearch configuration uses snake_case. To make it easier to match the log output of + // OpenSearch with this cluster configuration, snake_case is also used here. + #[serde(rename_all = "snake_case")] + pub enum NodeRole { + // Built-in node roles + // see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodeRole.java#L341-L346 + + // TODO https://github.com/Peternator7/strum/issues/113 + #[strum(serialize = "data")] + Data, + #[strum(serialize = "ingest")] + Ingest, + #[strum(serialize = "cluster-manager")] + ClusterManager, + #[strum(serialize = "remote-cluster-client")] + RemoteClusterClient, + #[strum(serialize = "warm")] + Warm, + + // Search node role + // see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodeRole.java#L313-L339 + #[strum(serialize = "search")] + Search, + } + + // #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] + // pub struct NR { + // data: Option<()>, + // ingest: Option<()>, + // cluster_manager: Option<()>, + // remote_cluster_client: Option<()>, + // warm: Option<()>, + // search: Option<()>, + // } + + #[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] + #[fragment_attrs( + derive( + Clone, + Debug, + Default, + Deserialize, + Merge, + JsonSchema, + PartialEq, + Serialize + ), + serde(rename_all = "camelCase") + )] + pub struct OpenSearchConfig { + pub node_roles: NodeRoles, + } + #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct OpenSearchClusterStatus { @@ -61,19 +133,25 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { } } -// TODO Perhaps rename to InstanceConfig -#[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] -#[fragment_attrs( - derive( - Clone, - Debug, - Default, - Deserialize, - Merge, - JsonSchema, - PartialEq, - Serialize - ), - serde(rename_all = "camelCase") -)] -pub struct OpenSearchConfig {} +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct NodeRoles(Vec); + +// impl Iterator for NodeRoles { +// type Item = v1alpha1::NodeRole; +// +// fn next(&mut self) -> Option { +// self. +// } +// } + +impl NodeRoles { + pub fn contains(&self, node_role: &v1alpha1::NodeRole) -> bool { + self.0.contains(node_role) + } + + pub fn iter(&self) -> slice::Iter { + self.0.iter() + } +} + +impl Atomic for NodeRoles {} diff --git a/tests/release.yaml b/tests/release.yaml new file mode 100644 index 0000000..c877cd4 --- /dev/null +++ b/tests/release.yaml @@ -0,0 +1,16 @@ +# Contains all operators required to run the test suite. +--- +releases: + # Do not change the name of the release as it's referenced from run-tests + tests: + releaseDate: 1970-01-01 + description: Integration test + products: + commons: + operatorVersion: 0.0.0-dev + secret: + operatorVersion: 0.0.0-dev + listener: + operatorVersion: 0.0.0-dev + opensearch: + operatorVersion: 0.0.0-dev diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml new file mode 100644 index 0000000..242ace8 --- /dev/null +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -0,0 +1,102 @@ +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + cluster-manager: "true" + stackable.tech/vendor: Stackable + name: opensearch-nodes-cluster-manager +spec: + podManagementPolicy: Parallel + replicas: 3 + template: + spec: + containers: + - name: opensearch + env: + - name: discovery.seed_hosts + value: opensearch-cluster-manager + - name: discovery.type + value: zen + - name: cluster.initial_cluster_manager_nodes + value: opensearch-nodes-cluster-manager-0,opensearch-nodes-cluster-manager-1,opensearch-nodes-cluster-manager-2 + - name: network.host + value: 0.0.0.0 + - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD + value: super@Secret1 + - name: node.name + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: node.roles + value: cluster-manager +status: + readyReplicas: 3 + replicas: 3 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + data: "true" + ingest: "true" + remote-cluster-client: "true" + stackable.tech/vendor: Stackable + name: opensearch-nodes-data +spec: + podManagementPolicy: Parallel + replicas: 5 + template: + spec: + containers: + - name: opensearch + env: + - name: discovery.seed_hosts + value: opensearch-cluster-manager + - name: discovery.type + value: zen + - name: cluster.initial_cluster_manager_nodes + - name: network.host + value: 0.0.0.0 + - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD + value: super@Secret1 + - name: node.name + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: node.roles + value: ingest,data,remote-cluster-client +status: + readyReplicas: 5 + replicas: 5 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + name: opensearch-nodes +spec: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 3aed8a0..480bfd6 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -9,5 +9,15 @@ spec: productVersion: 3.0.0 nodes: roleGroups: - default: - replicas: 1 + cluster-manager: + config: + nodeRoles: + - cluster_manager + replicas: 3 + data: + config: + nodeRoles: + - ingest + - data + - remote_cluster_client + replicas: 5 diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml new file mode 100644 index 0000000..505167a --- /dev/null +++ b/tests/test-definition.yaml @@ -0,0 +1,34 @@ +--- +dimensions: + - name: opensearch + values: + - 3.0.0 + - name: openshift + values: + - "false" +tests: + - name: smoke + dimensions: + - opensearch + - openshift +suites: + - name: nightly + patch: + - dimensions: + - name: opensearch + expr: last + - name: smoke-latest + select: + - smoke + patch: + - dimensions: + - expr: last + - name: openshift + patch: + - dimensions: + - expr: last + - dimensions: + - name: openshift + expr: "true" + - name: opensearch + expr: last From 99a172f9add40c311c0f145d2ff8503424143186 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Jun 2025 17:27:24 +0200 Subject: [PATCH 14/35] Implement overrides and set up first working cluster --- Cargo.lock | 152 +++++---- Cargo.nix | 171 +++++----- Cargo.toml | 1 + .../helm/opensearch-operator/crds/crds.yaml | 193 +++++++++++ rust/operator-binary/Cargo.toml | 1 + rust/operator-binary/src/controller.rs | 30 +- rust/operator-binary/src/controller/apply.rs | 3 + rust/operator-binary/src/controller/build.rs | 237 ++++++++++---- .../src/controller/node_config.rs | 175 +++++++++- .../src/controller/validate.rs | 12 +- rust/operator-binary/src/crd/mod.rs | 11 +- rust/operator-binary/src/framework/builder.rs | 1 + .../src/framework/builder/pod.rs | 1 + .../src/framework/builder/pod/container.rs | 77 +++++ .../src/framework/role_utils.rs | 162 +++++++++- .../kuttl/smoke/10-install-opensearch.yaml | 73 +++++ .../smoke/10-opensearch-security-config.yaml | 306 ++++++++++++++++++ .../kuttl/smoke/run-securityadmin.yaml | 61 ++++ 18 files changed, 1403 insertions(+), 264 deletions(-) create mode 100644 rust/operator-binary/src/framework/builder/pod.rs create mode 100644 rust/operator-binary/src/framework/builder/pod/container.rs create mode 100644 tests/templates/kuttl/smoke/10-opensearch-security-config.yaml create mode 100644 tests/templates/kuttl/smoke/run-securityadmin.yaml diff --git a/Cargo.lock b/Cargo.lock index 001739c..bc5c0d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -158,7 +158,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -169,9 +169,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" @@ -313,9 +313,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.26" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -372,7 +372,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -515,7 +515,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -526,7 +526,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -537,7 +537,7 @@ checksum = "b9b6483c2bbed26f97861cf57651d4f2b731964a28cd2257f934a4b452480d21" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -566,7 +566,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -587,7 +587,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -625,7 +625,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -669,7 +669,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -809,7 +809,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -1505,7 +1505,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -1543,15 +1543,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libgit2-sys" -version = "0.18.1+1.9.0" +version = "0.18.2+1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" dependencies = [ "cc", "libc", @@ -1846,9 +1846,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -1857,9 +1857,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -1867,24 +1867,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -1906,7 +1905,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -1996,7 +1995,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2010,9 +2009,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -2075,9 +2074,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags", ] @@ -2182,9 +2181,9 @@ checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "log", "once_cell", @@ -2292,7 +2291,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2379,7 +2378,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2390,7 +2389,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2488,12 +2487,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" @@ -2540,7 +2536,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2566,6 +2562,7 @@ dependencies = [ "built", "clap", "futures 0.3.31", + "schemars", "serde", "serde_json", "snafu 0.8.6", @@ -2620,7 +2617,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2684,7 +2681,7 @@ dependencies = [ "kube", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2712,7 +2709,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2734,9 +2731,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.102" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -2760,7 +2757,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2789,7 +2786,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -2800,17 +2797,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -2880,7 +2876,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -3041,13 +3037,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -3256,7 +3252,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -3291,7 +3287,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3368,7 +3364,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -3379,14 +3375,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -3529,28 +3525,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] [[package]] @@ -3570,7 +3566,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", "synstructure", ] @@ -3610,5 +3606,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.102", + "syn 2.0.104", ] diff --git a/Cargo.nix b/Cargo.nix index a557c35..f7c90e5 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -457,7 +457,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" "visit-mut" ]; } ]; @@ -484,7 +484,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "clone-impls" "full" "parsing" "printing" "proc-macro" "visit-mut" ]; } @@ -507,9 +507,9 @@ rec { }; "autocfg" = rec { crateName = "autocfg"; - version = "1.4.0"; + version = "1.5.0"; edition = "2015"; - sha256 = "09lz3by90d2hphbq56znag9v87gfpd9gb8nr82hll8z6x2nhprdc"; + sha256 = "1s77f98id9l4af4alklmzq46f21c980v13z2r1pcxx6bqgw0d1n0"; authors = [ "Josh Stone " ]; @@ -989,9 +989,9 @@ rec { }; "bumpalo" = rec { crateName = "bumpalo"; - version = "3.18.1"; + version = "3.19.0"; edition = "2021"; - sha256 = "1vmfniqr484l4ffkf0056g6hakncr7kdh11hyggh9kc7c5nvfgbr"; + sha256 = "0hsdndvcpqbjb85ghrhska2qxvp9i75q2vb70hma9fxqawdy9ia6"; authors = [ "Nick Fitzgerald " ]; @@ -1020,9 +1020,9 @@ rec { }; "cc" = rec { crateName = "cc"; - version = "1.2.26"; + version = "1.2.27"; edition = "2018"; - sha256 = "1b5g9ln7a2imwhrvfi77qbmj7gxsg0xihrlvarrg71wbk0hmwslm"; + sha256 = "1p5zfsl2mw3j46w58j2sxqkbfi49azilis5335pxlr2z3c3sm1yl"; authors = [ "Alex Crichton " ]; @@ -1227,7 +1227,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -1606,7 +1606,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" "extra-traits" ]; } ]; @@ -1636,7 +1636,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; @@ -1662,7 +1662,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" "visit-mut" ]; } ]; @@ -1764,7 +1764,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; features = { @@ -1840,7 +1840,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; features = { @@ -1933,13 +1933,13 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; devDependencies = [ { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -2042,7 +2042,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; features = { @@ -2445,7 +2445,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -4950,7 +4950,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; @@ -5097,9 +5097,9 @@ rec { }; "libc" = rec { crateName = "libc"; - version = "0.2.172"; + version = "0.2.174"; edition = "2021"; - sha256 = "1ykz4skj7gac14znljm5clbnrhini38jkq3d60jggx3y5w2ayl6p"; + sha256 = "0xl7pqvw7g2874dy3kjady2fjr4rhj5lxsnxkkhr5689jcr6jw8i"; authors = [ "The Rust Project Developers" ]; @@ -5113,10 +5113,10 @@ rec { }; "libgit2-sys" = rec { crateName = "libgit2-sys"; - version = "0.18.1+1.9.0"; + version = "0.18.2+1.9.1"; edition = "2018"; links = "git2"; - sha256 = "03i98nb84aa99bn7sxja11pllq6fghsaw4d3qwjxikgzhh7v5p71"; + sha256 = "08n223x2pkf4gj6yrjmh3z6q236qj6nifwww78xcblrbvw1zwhhw"; libName = "libgit2_sys"; libPath = "lib.rs"; authors = [ @@ -6128,9 +6128,9 @@ rec { }; "pest" = rec { crateName = "pest"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "1dp741bxqiracvvwl66mfvlr29byvvph28n4c6ip136m652vg38r"; + sha256 = "08s342r6vv6ml5in4jk7pb97wgpf0frcnrvg0sqshn23sdb5zc0x"; authors = [ "Dragoș Tiselice " ]; @@ -6162,9 +6162,9 @@ rec { }; "pest_derive" = rec { crateName = "pest_derive"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "1icp5i01mgpbgwbkrcy4d0ykbxmns4wyz8j1jg6dr1wysz7xj9fp"; + sha256 = "1g20ma4y29axbjhi3z64ihhpqzmiix71qjn7bs224yd7isg6s1dv"; procMacro = true; authors = [ "Dragoș Tiselice " @@ -6191,9 +6191,9 @@ rec { }; "pest_generator" = rec { crateName = "pest_generator"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "0hgqngsxfr8y5p47bgjvd038j55ix1x4dpzr6amndaz8ddr02zfv"; + sha256 = "0rj9a20g4bjb4sl3zyzpxqg8mbn8c1kxp0nw08rfp0gp73k09r47"; authors = [ "Dragoș Tiselice " ]; @@ -6217,7 +6217,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; features = { @@ -6230,17 +6230,13 @@ rec { }; "pest_meta" = rec { crateName = "pest_meta"; - version = "2.8.0"; + version = "2.8.1"; edition = "2021"; - sha256 = "182w5fyiqm7zbn0p8313xc5wc73rnn59ycm5zk8hcja9f0j877vz"; + sha256 = "1mf01iln7shbnyxpdfnpf59gzn83nndqjkwiw3yh6n8g2wgi1lgd"; authors = [ "Dragoș Tiselice " ]; dependencies = [ - { - name = "once_cell"; - packageId = "once_cell"; - } { name = "pest"; packageId = "pest"; @@ -6290,7 +6286,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "parsing" "printing" "clone-impls" "proc-macro" "full" "visit-mut" ]; } @@ -6525,7 +6521,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; @@ -6554,15 +6550,14 @@ rec { }; "r-efi" = rec { crateName = "r-efi"; - version = "5.2.0"; + version = "5.3.0"; edition = "2018"; - sha256 = "1ig93jvpqyi87nc5kb6dri49p56q7r7qxrn8kfizmqkfj5nmyxkl"; + sha256 = "03sbfm3g7myvzyylff6qaxk4z6fy76yv860yy66jiswc2m6b7kb9"; libName = "r_efi"; features = { - "compiler_builtins" = [ "dep:compiler_builtins" ]; "core" = [ "dep:core" ]; "examples" = [ "native" ]; - "rustc-dep-of-std" = [ "compiler_builtins/rustc-dep-of-std" "core" ]; + "rustc-dep-of-std" = [ "core" ]; }; }; "rand 0.8.5" = rec { @@ -6758,9 +6753,9 @@ rec { }; "redox_syscall" = rec { crateName = "redox_syscall"; - version = "0.5.12"; + version = "0.5.13"; edition = "2021"; - sha256 = "1by5k76jr4kjy37287ifn56dzw6jh6nrwnrjm29j615ayafcm3wj"; + sha256 = "1mlzna9bcd7ss1973bmysr3hpjrys82b3bd7l03h4jkbxv8bf10d"; libName = "syscall"; authors = [ "Jeremy Soller " @@ -7263,9 +7258,9 @@ rec { }; "rustls" = rec { crateName = "rustls"; - version = "0.23.27"; + version = "0.23.28"; edition = "2021"; - sha256 = "08d3nipyhmy4apksjyrb98s9k91wjyg1k7y0flx2671w135482bk"; + sha256 = "0hv6sk3r60vw11in2p8phpjd132684b4wg3zac456lzl1ghy6q3i"; dependencies = [ { name = "log"; @@ -7609,7 +7604,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; @@ -7863,7 +7858,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" "proc-macro" ]; } @@ -7895,7 +7890,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "clone-impls" "derive" "parsing" "printing" ]; } @@ -8162,18 +8157,12 @@ rec { }; "slab" = rec { crateName = "slab"; - version = "0.4.9"; + version = "0.4.10"; edition = "2018"; - sha256 = "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg"; + sha256 = "03f5a9gdp33mngya4qwq2555138pj74pl015scv57wsic5rikp04"; authors = [ "Carl Lerche " ]; - buildDependencies = [ - { - name = "autocfg"; - packageId = "autocfg"; - } - ]; features = { "default" = [ "std" ]; "serde" = [ "dep:serde" ]; @@ -8320,7 +8309,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -8393,6 +8382,10 @@ rec { packageId = "futures 0.3.31"; features = [ "compat" ]; } + { + name = "schemars"; + packageId = "schemars"; + } { name = "serde"; packageId = "serde"; @@ -8624,7 +8617,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; @@ -8887,7 +8880,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; features = { @@ -8958,7 +8951,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "parsing" ]; } ]; @@ -9011,11 +9004,11 @@ rec { }; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "full" "parsing" "printing" "proc-macro" "quote" ]; }; - "syn 2.0.102" = rec { + "syn 2.0.104" = rec { crateName = "syn"; - version = "2.0.102"; + version = "2.0.104"; edition = "2021"; - sha256 = "0qh4v2nj61y82cc713fakjckhmwyvllq9n0gpmcg147sjjppsfgn"; + sha256 = "0h2s8cxh5dsh9h41dxnlzpifqqn59cqgm0kljawws61ljq2zgdhp"; authors = [ "David Tolnay " ]; @@ -9087,7 +9080,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "derive" "parsing" "printing" "clone-impls" "visit" "extra-traits" ]; } @@ -9154,7 +9147,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; @@ -9180,16 +9173,16 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; } ]; }; "thread_local" = rec { crateName = "thread_local"; - version = "1.1.8"; + version = "1.1.9"; edition = "2021"; - sha256 = "173i5lyjh011gsimk21np9jn8al18rxsrkjli20a7b8ks2xgk7lb"; + sha256 = "1191jvl8d63agnq06pcnarivf63qzgpws5xa33hgc92gjjj4c0pn"; authors = [ "Amanieu d'Antras " ]; @@ -9198,10 +9191,6 @@ rec { name = "cfg-if"; packageId = "cfg-if"; } - { - name = "once_cell"; - packageId = "once_cell"; - } ]; features = { }; @@ -9478,7 +9467,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -10223,9 +10212,9 @@ rec { }; "tracing-attributes" = rec { crateName = "tracing-attributes"; - version = "0.1.29"; + version = "0.1.30"; edition = "2018"; - sha256 = "0qpn22v675pbgmrkjsx3abj6lr5s12v4wi77hv9rjsvgkk7zn7qv"; + sha256 = "00v9bhfgfg3v101nmmy7s3vdwadb7ngc8c1iw6wai9vj9sv3lf41"; procMacro = true; libName = "tracing_attributes"; authors = [ @@ -10244,7 +10233,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ]; } @@ -10903,7 +10892,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } { @@ -11004,7 +10993,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "visit" "visit-mut" "full" ]; } { @@ -11655,7 +11644,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -11685,7 +11674,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; usesDefaultFeatures = false; features = [ "parsing" "proc-macro" "printing" "full" "clone-impls" ]; } @@ -11694,9 +11683,9 @@ rec { }; "windows-link" = rec { crateName = "windows-link"; - version = "0.1.1"; + version = "0.1.3"; edition = "2021"; - sha256 = "0f2cq7imbrppsmmnz8899hfhg07cp5gq6rh0bjhb1qb6nwshk13n"; + sha256 = "12kr1p46dbhpijr4zbwr2spfgq8i8c5x55mvvfmyl96m01cx4sjy"; libName = "windows_link"; authors = [ "Microsoft" @@ -12498,7 +12487,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "fold" ]; } { @@ -12510,9 +12499,9 @@ rec { }; "zerocopy" = rec { crateName = "zerocopy"; - version = "0.8.25"; + version = "0.8.26"; edition = "2021"; - sha256 = "1jx07cd3b3456c9al9zjqqdzpf1abb0vf6z0fj8xnb93hfajsw51"; + sha256 = "0bvsj0qzq26zc6nlrm3z10ihvjspyngs7n0jw1fz031i7h6xsf8h"; authors = [ "Joshua Liebow-Feeser " "Jack Wrenn " @@ -12546,9 +12535,9 @@ rec { }; "zerocopy-derive" = rec { crateName = "zerocopy-derive"; - version = "0.8.25"; + version = "0.8.26"; edition = "2021"; - sha256 = "1vsmpq0hp61xpqj9yk8b5jihkqkff05q1wv3l2568mhifl6y59i8"; + sha256 = "10aiywi5qkha0mpsnb1zjwi44wl2rhdncaf3ykbp4i9nqm65pkwy"; procMacro = true; libName = "zerocopy_derive"; authors = [ @@ -12566,7 +12555,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "full" ]; } ]; @@ -12615,7 +12604,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "fold" ]; } { @@ -12745,7 +12734,7 @@ rec { } { name = "syn"; - packageId = "syn 2.0.102"; + packageId = "syn 2.0.104"; features = [ "extra-traits" ]; } ]; diff --git a/Cargo.toml b/Cargo.toml index 8f9ee56..19dce34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", built = { version = "0.8.0", features = ["chrono", "git2"] } clap = "4.5" futures = { version = "0.3", features = ["compat"] } +schemars = { version = "0.8.21" } # same as in operator-rs serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" snafu = "0.8" diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 46f9b81..6782010 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -24,6 +24,199 @@ spec: properties: spec: description: A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. Find more information on how to use it and the resources that the operator generates in the [operator documentation](https://docs.stackable.tech/home/nightly/opensearch/). + properties: + clusterOperation: + default: + reconciliationPaused: false + stopped: false + description: '[Cluster operations](https://docs.stackable.tech/home/nightly/concepts/operations/cluster_operations) properties, allow stopping the product instance as well as pausing reconciliation.' + properties: + reconciliationPaused: + default: false + description: Flag to stop cluster reconciliation by the operator. This means that all changes in the custom resource spec are ignored until this flag is set to false or removed. The operator will however still watch the deployed resources at the time and update the custom resource status field. If applied at the same time with `stopped`, `reconciliationPaused` will take precedence over `stopped` and stop the reconciliation immediately. + type: boolean + stopped: + default: false + description: Flag to stop the cluster. This means all deployed resources (e.g. Services, StatefulSets, ConfigMaps) are kept but all deployed Pods (e.g. replicas from a StatefulSet) are scaled to 0 and therefore stopped and removed. If applied at the same time with `reconciliationPaused`, the latter will pause reconciliation and `stopped` will take no effect until `reconciliationPaused` is set to false or removed. + type: boolean + type: object + image: + anyOf: + - required: + - custom + - productVersion + - required: + - productVersion + description: |- + Specify which image to use, the easiest way is to only configure the `productVersion`. You can also configure a custom image registry to pull from, as well as completely custom images. + + Consult the [Product image selection documentation](https://docs.stackable.tech/home/nightly/concepts/product_image_selection) for details. + properties: + custom: + description: Overwrite the docker image. Specify the full docker image name, e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` + type: string + productVersion: + description: Version of the product, e.g. `1.4.1`. + type: string + pullPolicy: + default: Always + description: '[Pull policy](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) used when pulling the image.' + enum: + - IfNotPresent + - Always + - Never + type: string + pullSecrets: + description: '[Image pull secrets](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) to pull images from a private registry.' + items: + description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + properties: + name: + description: 'Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - name + type: object + nullable: true + type: array + repo: + description: Name of the docker repo, e.g. `oci.stackable.tech/sdp` + nullable: true + type: string + stackableVersion: + description: Stackable version of the product, e.g. `23.4`, `23.4.1` or `0.0.0-dev`. If not specified, the operator will use its own version, e.g. `23.4.1`. When using a nightly operator or a pr version, it will use the nightly `0.0.0-dev` image. + nullable: true + type: string + type: object + nodes: + description: OpenSearch nodes + properties: + cliOverrides: + additionalProperties: + type: string + default: {} + type: object + config: + default: {} + properties: + nodeRoles: + items: + enum: + - data + - ingest + - cluster_manager + - remote_cluster_client + - warm + - search + type: string + nullable: true + type: array + type: object + configOverrides: + additionalProperties: + additionalProperties: + type: string + type: object + default: {} + description: The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. + type: object + envOverrides: + additionalProperties: + type: string + default: {} + description: '`envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available.' + type: object + podOverrides: + default: {} + description: In the `podOverrides` property you can define a [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) for more information. + type: object + x-kubernetes-preserve-unknown-fields: true + roleConfig: + default: + podDisruptionBudget: + enabled: true + maxUnavailable: null + description: This is a product-agnostic RoleConfig, which is sufficient for most of the products. + properties: + podDisruptionBudget: + default: + enabled: true + maxUnavailable: null + description: |- + This struct is used to configure: + + 1. If PodDisruptionBudgets are created by the operator 2. The allowed number of Pods to be unavailable (`maxUnavailable`) + + Learn more in the [allowed Pod disruptions documentation](https://docs.stackable.tech/home/nightly/concepts/operations/pod_disruptions). + properties: + enabled: + default: true + description: Whether a PodDisruptionBudget should be written out for this role. Disabling this enables you to specify your own - custom - one. Defaults to true. + type: boolean + maxUnavailable: + description: The number of Pods that are allowed to be down because of voluntary disruptions. If you don't explicitly set this, the operator will use a sane default based upon knowledge about the individual product. + format: uint16 + minimum: 0.0 + nullable: true + type: integer + type: object + type: object + roleGroups: + additionalProperties: + properties: + cliOverrides: + additionalProperties: + type: string + default: {} + type: object + config: + default: {} + properties: + nodeRoles: + items: + enum: + - data + - ingest + - cluster_manager + - remote_cluster_client + - warm + - search + type: string + nullable: true + type: array + type: object + configOverrides: + additionalProperties: + additionalProperties: + type: string + type: object + default: {} + description: The `configOverrides` can be used to configure properties in product config files that are not exposed in the CRD. Read the [config overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#config-overrides) and consult the operator specific usage guide documentation for details on the available config files and settings for the specific product. + type: object + envOverrides: + additionalProperties: + type: string + default: {} + description: '`envOverrides` configure environment variables to be set in the Pods. It is a map from strings to strings - environment variables and the value to set. Read the [environment variable overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#env-overrides) for more information and consult the operator specific usage guide to find out about the product specific environment variables that are available.' + type: object + podOverrides: + default: {} + description: In the `podOverrides` property you can define a [PodTemplateSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#podtemplatespec-v1-core) to override any property that can be set on a Kubernetes Pod. Read the [Pod overrides documentation](https://docs.stackable.tech/home/nightly/concepts/overrides#pod-overrides) for more information. + type: object + x-kubernetes-preserve-unknown-fields: true + replicas: + format: uint16 + minimum: 0.0 + nullable: true + type: integer + type: object + type: object + required: + - roleGroups + type: object + required: + - image + - nodes type: object status: nullable: true diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index d6cc505..9d97df9 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -14,6 +14,7 @@ stackable-operator.workspace = true clap.workspace = true futures.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true snafu.workspace = true diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index f28eed0..830df49 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -6,10 +6,14 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, commons::product_image_selection::ProductImage, - k8s_openapi::api::{apps::v1::StatefulSet, core::v1::Service, policy::v1::PodDisruptionBudget}, - kube::{Resource, core::DeserializeGuard, runtime::controller::Action}, + k8s_openapi::api::{ + apps::v1::StatefulSet, + core::v1::{ConfigMap, Service}, + policy::v1::PodDisruptionBudget, + }, + kube::{Resource, api::ObjectMeta, core::DeserializeGuard, runtime::controller::Action}, logging::controller::ReconcilerError, - role_utils::{GenericProductSpecificCommonConfig, GenericRoleConfig, RoleGroup}, + role_utils::GenericRoleConfig, time::Duration, }; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -21,6 +25,7 @@ use crate::{ framework::{ ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, ProductName, ProductVersion, RoleGroupName, + role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, }, }; @@ -90,14 +95,16 @@ impl ReconcilerError for Error { } } -type RoleGroupConfig = RoleGroup; +type OpenSearchRoleGroupConfig = + RoleGroupConfig; // validated and converted to validated and safe types // no user errors // not restricted by CRD compliance +// TODO More derives #[derive(Clone)] pub struct ValidatedCluster { - origin: v1alpha1::OpenSearchCluster, + metadata: ObjectMeta, pub image: ProductImage, pub product_version: ProductVersion, pub name: ClusterName, @@ -105,7 +112,7 @@ pub struct ValidatedCluster { pub uid: String, pub role_config: GenericRoleConfig, // "validated" means that labels are valid and no ugly rolegroup name broke them - pub role_group_configs: BTreeMap, + pub role_group_configs: BTreeMap, } impl ValidatedCluster { @@ -116,18 +123,18 @@ impl ValidatedCluster { pub fn node_count(&self) -> u32 { self.role_group_configs .values() - .map(|rg| rg.replicas.unwrap_or(1) as u32) + .map(|rg| rg.replicas as u32) .sum() } pub fn role_group_configs_filtered_by_node_role( &self, node_role: &v1alpha1::NodeRole, - ) -> BTreeMap { + ) -> BTreeMap { self.role_group_configs .clone() .into_iter() - .filter(|c| c.1.config.config.node_roles.contains(node_role)) + .filter(|c| c.1.config.node_roles.contains(node_role)) .collect() } } @@ -181,11 +188,11 @@ impl Resource for ValidatedCluster { } fn meta(&self) -> &stackable_operator::kube::api::ObjectMeta { - self.origin.meta() + &self.metadata } fn meta_mut(&mut self) -> &mut stackable_operator::kube::api::ObjectMeta { - self.origin.meta_mut() + &mut self.metadata } } @@ -249,6 +256,7 @@ struct Applied; struct Resources { stateful_sets: Vec, services: Vec, + config_maps: Vec, pod_disruption_budgets: Vec, status: PhantomData, } diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 9c849c4..152ec1d 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -59,6 +59,8 @@ impl<'a> Applier<'a> { let services = self.add_resources(resources.services).await?; + let config_maps = self.add_resources(resources.config_maps).await?; + let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?; self.cluster_resources @@ -69,6 +71,7 @@ impl<'a> Applier<'a> { Ok(Resources { stateful_sets, services, + config_maps, pod_disruption_budgets, status: PhantomData, }) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 7748270..7661790 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -2,37 +2,34 @@ use std::{marker::PhantomData, str::FromStr}; use stackable_operator::{ builder::{ + configmap::ConfigMapBuilder, meta::ObjectMetaBuilder, - pod::{ - PodBuilder, - container::{ContainerBuilder, FieldPathEnvVar}, - }, + pod::{PodBuilder, container::ContainerBuilder}, }, k8s_openapi::{ + DeepMerge, api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ - Container, ContainerPort, PodTemplateSpec, Service, ServicePort, ServiceSpec, + ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodTemplateSpec, + Service, ServicePort, ServiceSpec, Volume, VolumeMount, }, policy::v1::PodDisruptionBudget, }, apimachinery::pkg::apis::meta::v1::LabelSelector, }, kvp::{ - Label, Labels, + Label, Labels, ObjectLabels, consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, }, }; use super::{ - ContextNames, Prepared, Resources, RoleGroupConfig, RoleGroupName, ValidatedCluster, - node_config::{ - DISCOVERY_SEED_HOSTS, DISCOVERY_TYPE, INITIAL_CLUSTER_MANAGER_NODES, NETWORK_HOST, - NODE_NAME, NODE_ROLES, NodeConfig, - }, + ContextNames, OpenSearchRoleGroupConfig, Prepared, Resources, RoleGroupName, ValidatedCluster, + node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}, }; use crate::framework::{ - RoleName, + IsLabelValue, RoleName, builder::pdb::pod_disruption_budget_builder_with_role, kvp::label::{recommended_labels, role_group_selector}, to_qualified_role_group_name, @@ -40,6 +37,7 @@ use crate::framework::{ const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; +// TODO Convert to RoleGroupBuilder pub struct Builder<'a> { names: &'a ContextNames, role_name: RoleName, @@ -59,45 +57,94 @@ impl<'a> Builder<'a> { } pub fn build(&self) -> Resources { - let stateful_sets = self - .cluster - .role_group_configs - .iter() - .map(|(role_group_name, role_group_config)| { - self.build_statefulset(role_group_name, role_group_config) - }) - .collect(); + let mut config_maps = vec![]; + let mut stateful_sets = vec![]; + let mut services = vec![]; - let services = vec![self.build_cluster_manager_service()]; + for (role_group_name, role_group_config) in &self.cluster.role_group_configs { + // used for the name of the StatefulSet, role-group ConfigMap, ... + let qualified_role_group_name = + to_qualified_role_group_name(&self.cluster.name, &self.role_name, role_group_name); - // TODO Create further services + let config_map = self.build_role_group_config_map( + &qualified_role_group_name, + role_group_name, + role_group_config, + ); + let stateful_set = self.build_statefulset( + &qualified_role_group_name, + role_group_name, + role_group_config, + ); + + let service = + self.build_role_group_service(&qualified_role_group_name, role_group_name); + + config_maps.push(config_map); + stateful_sets.push(stateful_set); + services.push(service); + } + + let cluster_manager_service = self.build_cluster_manager_service(); + services.push(cluster_manager_service); let pod_disruption_budgets = self.build_pdb().into_iter().collect(); Resources { stateful_sets, services, + config_maps, pod_disruption_budgets, status: PhantomData, } } + fn build_role_group_config_map( + &self, + config_map_name: &str, + role_group_name: &RoleGroupName, + role_group_config: &OpenSearchRoleGroupConfig, + ) -> ConfigMap { + let metadata = ObjectMetaBuilder::new() + .name(config_map_name) + .namespace(&self.cluster.namespace) + .ownerreference_from_resource(&self.cluster, None, Some(true)) + // TODO Fix + .unwrap() + .with_labels(self.build_recommended_labels(role_group_name)) + .build(); + + ConfigMapBuilder::new() + .metadata(metadata) + .add_data( + CONFIGURATION_FILE_OPENSEARCH_YML, + self.node_config.static_opensearch_config(role_group_config), + ) + .build() + // TODO Fix + .unwrap() + } + fn build_statefulset( &self, + qualified_role_group_name: &str, role_group_name: &RoleGroupName, - role_group_config: &RoleGroupConfig, + role_group_config: &OpenSearchRoleGroupConfig, ) -> StatefulSet { let metadata = ObjectMetaBuilder::new() - .name(to_qualified_role_group_name( - &self.cluster.name, - &self.role_name, - role_group_name, - )) + .name(qualified_role_group_name) .namespace(&self.cluster.namespace) + .ownerreference_from_resource(&self.cluster, None, Some(true)) + // TODO Fix + .unwrap() .with_labels(self.build_recommended_labels(role_group_name)) .build(); - let template = self.build_pod_template(role_group_name, role_group_config); + let template = self.build_pod_template( + qualified_role_group_name, + role_group_name, + role_group_config, + ); let statefulset_match_labels = role_group_selector( &self.cluster, @@ -109,7 +156,7 @@ impl<'a> Builder<'a> { let spec = StatefulSetSpec { // Order does not matter for OpenSearch pod_management_policy: Some("Parallel".to_string()), - replicas: role_group_config.replicas.map(i32::from), + replicas: Some(role_group_config.replicas as i32), selector: LabelSelector { match_labels: Some(statefulset_match_labels.into()), ..LabelSelector::default() @@ -119,8 +166,6 @@ impl<'a> Builder<'a> { ..StatefulSetSpec::default() }; - // TODO Implement overrides - StatefulSet { metadata, spec: Some(spec), @@ -130,14 +175,14 @@ impl<'a> Builder<'a> { fn build_pod_template( &self, + qualified_role_group_name: &str, role_group_name: &RoleGroupName, - role_group_config: &RoleGroupConfig, + role_group_config: &OpenSearchRoleGroupConfig, ) -> PodTemplateSpec { let mut builder = PodBuilder::new(); - let opensearch_config = &role_group_config.config.config; let mut node_role_labels = Labels::new(); - for node_role in opensearch_config.node_roles.iter() { + for node_role in role_group_config.config.node_roles.iter() { node_role_labels .insert(Label::try_from((format!("{node_role}"), "true".to_string())).unwrap()); } @@ -149,43 +194,54 @@ impl<'a> Builder<'a> { let container = self.build_container(role_group_config); - builder + let mut pod_template = builder .metadata(metadata) .add_container(container) - .build_template() + .add_volume(Volume { + name: "config".to_string(), + config_map: Some(ConfigMapVolumeSource { + name: qualified_role_group_name.to_owned(), + ..Default::default() + }), + ..Default::default() + }) + // TODO ? + .unwrap() + .build_template(); + + pod_template.merge_from(role_group_config.pod_overrides.clone()); + + pod_template } - fn build_container(&self, role_group_config: &RoleGroupConfig) -> Container { + fn build_container(&self, role_group_config: &OpenSearchRoleGroupConfig) -> Container { let product_image = self .cluster .image .resolve("opensearch", crate::built_info::PKG_VERSION); - let opensearch_config = &role_group_config.config.config; - ContainerBuilder::new("opensearch") .expect("should be a valid container name") .image_from_product_image(&product_image) - .add_env_var( - DISCOVERY_SEED_HOSTS, - self.node_config.discovery_seed_hosts(), - ) - .add_env_var(DISCOVERY_TYPE, self.node_config.discovery_type()) - .add_env_var( - INITIAL_CLUSTER_MANAGER_NODES, + .command(vec![ + "/usr/share/opensearch/opensearch-docker-entrypoint.sh".to_owned(), + ]) + .args(role_group_config.cli_overrides_to_vec()) + .add_env_vars( self.node_config - .initial_cluster_manager_nodes(&opensearch_config.node_roles), - ) - .add_env_var(NETWORK_HOST, self.node_config.network_host()) - // TODO Is this option also required on a proper custom image? - .add_env_var("OPENSEARCH_INITIAL_ADMIN_PASSWORD", "super@Secret1") - // Set the OpenSearch node name to the Pod name. - // The node name is used e.g. for `{INITIAL_CLUSTER_MANAGER_NODES}`. - .add_env_var_from_field_path(NODE_NAME, FieldPathEnvVar::Name) - .add_env_var( - NODE_ROLES, - self.node_config.node_roles(&opensearch_config.node_roles), + .environment_variables(role_group_config) + .into(), ) + .add_volume_mounts([VolumeMount { + // TODO Use path and file constants + mount_path: "/usr/share/opensearch/config/opensearch.yml".to_owned(), + name: "config".to_owned(), + read_only: Some(true), + sub_path: Some("opensearch.yml".to_owned()), + ..VolumeMount::default() + }]) + // TODO ? + .unwrap() .add_container_ports(vec![ ContainerPort { name: Some("http".to_owned()), @@ -213,6 +269,71 @@ impl<'a> Builder<'a> { ) } + fn build_role_group_service( + &self, + qualified_role_group_name: &str, + role_group_name: &RoleGroupName, + ) -> Service { + let ports = vec![ + ServicePort { + name: Some("http".to_owned()), + port: 9200, + ..ServicePort::default() + }, + ServicePort { + name: Some("transport".to_owned()), + port: 9300, + ..ServicePort::default() + }, + ]; + + // TODO Add metrics port and Prometheus label + + let metadata = ObjectMetaBuilder::new() + .name(qualified_role_group_name) + .namespace(&self.cluster.namespace) + .ownerreference_from_resource(&self.cluster, None, Some(true)) + // TODO Fix + .unwrap() + .with_recommended_labels(ObjectLabels { + owner: &self.cluster, + app_name: &self.names.product_name.to_label_value(), + app_version: &self.cluster.product_version.to_label_value(), + operator_name: &self.names.operator_name.to_label_value(), + controller_name: &self.names.controller_name.to_label_value(), + role: &self.role_name.to_label_value(), + role_group: &role_group_name.to_label_value(), + }) + // TODO fix + .unwrap() + .build(); + + let service_selector = Labels::role_group_selector( + &self.cluster, + &self.names.product_name.to_label_value(), + &self.role_name.to_label_value(), + &role_group_name.to_label_value(), + ) + // TODO fix + .unwrap(); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(service_selector.into()), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, + } + } + fn build_cluster_manager_service(&self) -> Service { let ports = vec![ ServicePort { diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/node_config.rs index f5c8594..7b5d7ce 100644 --- a/rust/operator-binary/src/controller/node_config.rs +++ b/rust/operator-binary/src/controller/node_config.rs @@ -1,9 +1,15 @@ -use super::ValidatedCluster; +use std::collections::BTreeMap; + +use stackable_operator::builder::pod::container::FieldPathEnvVar; + +use super::{OpenSearchRoleGroupConfig, ValidatedCluster}; use crate::{ crd::{NodeRoles, v1alpha1}, - framework::{RoleName, to_qualified_role_group_name}, + framework::{RoleName, builder::pod::container::EnvVarSet, to_qualified_role_group_name}, }; +pub const CONFIGURATION_FILE_OPENSEARCH_YML: &str = "opensearch.yml"; +pub const CONFIG_OPTION_CLUSTER_NAME: &str = "cluster.name"; pub const DISCOVERY_SEED_HOSTS: &str = "discovery.seed_hosts"; pub const DISCOVERY_TYPE: &str = "discovery.type"; pub const INITIAL_CLUSTER_MANAGER_NODES: &str = "cluster.initial_cluster_manager_nodes"; @@ -16,14 +22,75 @@ pub struct NodeConfig { cluster: ValidatedCluster, } +// Most functions are public because their configuration values could also be used in environment +// variables. impl NodeConfig { pub fn new(role_name: RoleName, cluster: ValidatedCluster) -> Self { Self { role_name, cluster } } + /// static for the cluster + pub fn static_opensearch_config( + &self, + // TODO only config overrides + role_group_config: &OpenSearchRoleGroupConfig, + ) -> String { + let mut config: BTreeMap = [ + (CONFIG_OPTION_CLUSTER_NAME.to_owned(), self.cluster_name()), + (NETWORK_HOST.to_owned(), self.network_host()), + (DISCOVERY_TYPE.to_owned(), self.discovery_type()), + ] + .into(); + + config.extend( + role_group_config + .config_overrides + .get(CONFIGURATION_FILE_OPENSEARCH_YML) + .cloned() + .unwrap_or_default(), + ); + + NodeConfig::to_yaml(config) + } + + /// different for every node + pub fn environment_variables( + &self, + // only node roles? + role_group_config: &OpenSearchRoleGroupConfig, + ) -> EnvVarSet { + EnvVarSet::new() + // Set the OpenSearch node name to the Pod name. + // The node name is used e.g. for `{INITIAL_CLUSTER_MANAGER_NODES}`. + .with_field_path(NODE_NAME, FieldPathEnvVar::Name) + // TODO DISCOVERY_SEED_HOSTS to opensearch.yml? + .with_value(DISCOVERY_SEED_HOSTS, self.discovery_seed_hosts()) + .with_value( + INITIAL_CLUSTER_MANAGER_NODES, + self.initial_cluster_manager_nodes(&role_group_config.config.node_roles), + ) + .with_value( + NODE_ROLES, + self.node_roles(&role_group_config.config.node_roles), + ) + .with_values(role_group_config.env_overrides.clone()) + } + + fn to_yaml(kv: BTreeMap) -> String { + // TODO Do it right! + kv.iter() + .map(|(key, value)| format!("{key}: {value}")) + .collect::>() + .join("\n") + } + + pub fn cluster_name(&self) -> String { + self.cluster.name.to_string() + } + pub fn discovery_seed_hosts(&self) -> String { // TODO Fix - format!("{}-cluster-manager", self.cluster.name) + "opensearch-nodes-cluster-manager.default.svc.cluster.local".to_owned() } /// Configuration for `{DISCOVERY_TYPE}` @@ -31,6 +98,9 @@ impl NodeConfig { /// "zen" is the default if `{DISCOVERY_TYPE}` is not set. /// It is nevertheless explicitly set here. /// see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/discovery/DiscoveryModule.java#L88-L89 + /// + /// "single-node" disables the bootstrap checks, like validating the JVM and discovery + /// configurations. pub fn discovery_type(&self) -> String { if self.cluster.is_single_node() { "single-node".to_owned() @@ -77,9 +147,8 @@ impl NodeConfig { &self.role_name, &role_group_name, ); - pod_names.extend( - (0..role_group_config.replicas.unwrap_or(1)).map(|i| format!("{sts_name}-{i}")), - ); + pod_names + .extend((0..role_group_config.replicas).map(|i| format!("{sts_name}-{i}"))); } pod_names.join(",") } else { @@ -90,6 +159,7 @@ impl NodeConfig { } pub fn network_host(&self) -> String { + // Bind to all interfaces because the IP address is not known in advance. "0.0.0.0".to_owned() } @@ -101,3 +171,96 @@ impl NodeConfig { .join(",") } } + +#[cfg(test)] +mod tests { + + use std::{collections::HashMap, str::FromStr}; + + use stackable_operator::{ + commons::product_image_selection::ProductImage, + k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector, PodTemplateSpec}, + kube::api::ObjectMeta, + role_utils::GenericRoleConfig, + }; + + use super::*; + use crate::framework::{ + ClusterName, ProductVersion, role_utils::GenericProductSpecificCommonConfig, + }; + + #[test] + pub fn test_environment_variables() { + let image: ProductImage = serde_json::from_str(r#"{"productVersion": "3.0.0"}"#) + .expect("should be a valid ProductImage"); + let cluster = ValidatedCluster { + metadata: ObjectMeta::default(), + image: image.clone(), + product_version: ProductVersion::from_str(image.product_version()) + .expect("should be a valid ProductVersion"), + name: ClusterName::from_str("my-opensearch-cluster") + .expect("should be a valid ClusterName"), + namespace: "default".to_owned(), + uid: "0b1e30e6-326e-4c1a-868d-ad6598b49e8b".to_owned(), + role_config: GenericRoleConfig::default(), + role_group_configs: BTreeMap::new(), + }; + let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); + + let node_config = NodeConfig::new(role_name, cluster); + + let role_group_config = OpenSearchRoleGroupConfig { + replicas: 1, + config: v1alpha1::OpenSearchConfig { + node_roles: NodeRoles::default(), + }, + config_overrides: HashMap::default(), + env_overrides: [("TEST".to_owned(), "value".to_owned())].into(), + cli_overrides: BTreeMap::default(), + pod_overrides: PodTemplateSpec::default(), + product_specific_common_config: GenericProductSpecificCommonConfig::default(), + }; + + let env_vars = node_config.environment_variables(&role_group_config); + + // TODO Test EnvVarSet and compare EnvVarSets + assert_eq!( + vec![ + EnvVar { + name: "TEST".to_owned(), + value: Some("value".to_owned()), + value_from: None + }, + EnvVar { + name: "cluster.initial_cluster_manager_nodes".to_owned(), + value: Some("".to_owned()), + value_from: None + }, + EnvVar { + name: "discovery.seed_hosts".to_owned(), + value: Some("my-opensearch-cluster-cluster-manager".to_owned()), + value_from: None + }, + EnvVar { + name: "node.name".to_owned(), + value: None, + value_from: Some(EnvVarSource { + config_map_key_ref: None, + field_ref: Some(ObjectFieldSelector { + api_version: None, + field_path: "metadata.name".to_owned() + }), + resource_field_ref: None, + secret_key_ref: None + }) + }, + EnvVar { + name: "node.roles".to_owned(), + value: Some("".to_owned()), + value_from: None + } + ], + >>::into(env_vars) + ); + } +} diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index b8ffddc..a2d9596 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -7,7 +7,10 @@ use strum::{EnumDiscriminants, IntoStaticStr}; use super::{ProductVersion, RoleGroupName, ValidatedCluster}; use crate::{ crd::v1alpha1, - framework::{ClusterName, role_utils::with_validated_config}, + framework::{ + ClusterName, + role_utils::{RoleGroupConfig, with_validated_config}, + }, }; #[derive(Snafu, Debug, EnumDiscriminants)] @@ -57,18 +60,21 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result, + pub nodes: + Role, } // The possible node roles are by default the built-in roles and the search role, see @@ -72,9 +75,9 @@ pub mod versioned { Data, #[strum(serialize = "ingest")] Ingest, - #[strum(serialize = "cluster-manager")] + #[strum(serialize = "cluster_manager")] ClusterManager, - #[strum(serialize = "remote-cluster-client")] + #[strum(serialize = "remote_cluster_client")] RemoteClusterClient, #[strum(serialize = "warm")] Warm, diff --git a/rust/operator-binary/src/framework/builder.rs b/rust/operator-binary/src/framework/builder.rs index d3cf6e9..70d637e 100644 --- a/rust/operator-binary/src/framework/builder.rs +++ b/rust/operator-binary/src/framework/builder.rs @@ -1 +1,2 @@ pub mod pdb; +pub mod pod; diff --git a/rust/operator-binary/src/framework/builder/pod.rs b/rust/operator-binary/src/framework/builder/pod.rs new file mode 100644 index 0000000..18581c4 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/pod.rs @@ -0,0 +1 @@ +pub mod container; diff --git a/rust/operator-binary/src/framework/builder/pod/container.rs b/rust/operator-binary/src/framework/builder/pod/container.rs new file mode 100644 index 0000000..4d4d482 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/pod/container.rs @@ -0,0 +1,77 @@ +use std::collections::BTreeMap; + +use stackable_operator::{ + builder::pod::container::FieldPathEnvVar, + k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector}, +}; + +// TODO Use validated type +type EnvVarName = String; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct EnvVarSet(BTreeMap); + +impl EnvVarSet { + pub fn new() -> Self { + Self::default() + } + + pub fn with_values(self, env_vars: I) -> Self + where + I: IntoIterator, + K: Into, + V: Into, + { + env_vars + .into_iter() + .fold(self, |extended_env_vars, (name, value)| { + extended_env_vars.with_value(name, value) + }) + } + + pub fn with_value(mut self, name: impl Into, value: impl Into) -> Self { + let name: EnvVarName = name.into(); + + self.0.insert( + name.clone(), + EnvVar { + name, + value: Some(value.into()), + value_from: None, + }, + ); + + self + } + + pub fn with_field_path( + mut self, + name: impl Into, + field_path: FieldPathEnvVar, + ) -> Self { + let name: EnvVarName = name.into(); + + self.0.insert( + name.clone(), + EnvVar { + name, + value: None, + value_from: Some(EnvVarSource { + field_ref: Some(ObjectFieldSelector { + field_path: field_path.to_string(), + ..ObjectFieldSelector::default() + }), + ..EnvVarSource::default() + }), + }, + ); + + self + } +} + +impl From for Vec { + fn from(value: EnvVarSet) -> Self { + value.0.values().cloned().collect() + } +} diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs index d21cd45..e840589 100644 --- a/rust/operator-binary/src/framework/role_utils.rs +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -1,37 +1,173 @@ -use serde::Serialize; +use std::collections::{BTreeMap, HashMap}; + +use serde::{Deserialize, Serialize}; use stackable_operator::{ config::{ fragment::{self, FromFragment}, merge::Merge, }, + k8s_openapi::api::core::v1::PodTemplateSpec, role_utils::{CommonConfiguration, Role, RoleGroup}, schemars::JsonSchema, }; +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct GenericProductSpecificCommonConfig {} + +impl Merge for GenericProductSpecificCommonConfig { + fn merge(&mut self, _defaults: &Self) {} +} + +// much better to work with than RoleGroup +#[derive(Clone, Debug, PartialEq)] +pub struct RoleGroupConfig { + pub replicas: u16, + pub config: T, + pub config_overrides: HashMap>, + pub env_overrides: HashMap, + pub cli_overrides: BTreeMap, + pub pod_overrides: PodTemplateSpec, + // allow(dead_code) is not necessary anymore when moved to operator-rs + #[allow(dead_code)] + pub product_specific_common_config: ProductSpecificCommonConfig, +} + +impl RoleGroupConfig { + pub fn cli_overrides_to_vec(&self) -> Vec { + self.cli_overrides + .clone() + .into_iter() + .flat_map(|(option, value)| [option, value]) + .collect() + } +} + +impl From> + for RoleGroupConfig +{ + fn from(value: RoleGroup) -> Self { + RoleGroupConfig { + // Kubernetes defaults to 1 if not set + replicas: value.replicas.unwrap_or(1), + config: value.config.config, + config_overrides: value.config.config_overrides, + env_overrides: value.config.env_overrides, + cli_overrides: value.config.cli_overrides, + pod_overrides: value.config.pod_overrides, + product_specific_common_config: value.config.product_specific_common_config, + } + } +} + +// RoleGroup::validate_config with fixed types +pub fn validate_config( + role_group: &RoleGroup, + role: &Role, + default_config: &T, +) -> Result +where + C: FromFragment, + ProductSpecificCommonConfig: Default + JsonSchema + Serialize, + T: Merge + Clone, + U: Default + JsonSchema + Serialize, +{ + let mut role_config = role.config.config.clone(); + role_config.merge(default_config); + let mut rolegroup_config = role_group.config.config.clone(); + rolegroup_config.merge(&role_config); + fragment::validate(rolegroup_config) +} + +// also useful for operators which use the product config pub fn with_validated_config( role_group: &RoleGroup, - role: &Role, + role: &Role, default_config: &T, ) -> Result, fragment::ValidationError> where C: FromFragment, - ProductSpecificCommonConfig: Clone, - T: Merge + Clone, + ProductSpecificCommonConfig: Clone + Default + JsonSchema + Merge + Serialize, + T: Clone + Merge, U: Default + JsonSchema + Serialize, { - let validated_config = role_group.validate_config(role, default_config)?; + let validated_config = validate_config(role_group, role, default_config)?; Ok(RoleGroup { config: CommonConfiguration { config: validated_config, - config_overrides: role_group.config.config_overrides.clone(), - env_overrides: role_group.config.env_overrides.clone(), - cli_overrides: role_group.config.cli_overrides.clone(), - pod_overrides: role_group.config.pod_overrides.clone(), - product_specific_common_config: role_group - .config - .product_specific_common_config - .clone(), + config_overrides: merged_config_overrides( + role.config.config_overrides.clone(), + role_group.config.config_overrides.clone(), + ), + env_overrides: merged_env_overrides( + role.config.env_overrides.clone(), + role_group.config.env_overrides.clone(), + ), + cli_overrides: merged_cli_overrides( + role.config.cli_overrides.clone(), + role_group.config.cli_overrides.clone(), + ), + pod_overrides: merged_pod_overrides( + role.config.pod_overrides.clone(), + role_group.config.pod_overrides.clone(), + ), + // TODO Merge + product_specific_common_config: merged_product_specific_common_config( + role.config.product_specific_common_config.clone(), + role_group.config.product_specific_common_config.clone(), + ), }, replicas: role_group.replicas, }) } + +fn merged_config_overrides( + role_config_overrides: HashMap>, + role_group_config_overrides: HashMap>, +) -> HashMap> { + let mut merged_config_overrides = role_config_overrides; + + for (filename, role_group_config_file_overrides) in role_group_config_overrides { + merged_config_overrides + .entry(filename) + .or_default() + .extend(role_group_config_file_overrides); + } + + merged_config_overrides +} + +fn merged_env_overrides( + role_env_overrides: HashMap, + role_group_env_overrides: HashMap, +) -> HashMap { + let mut merged_env_overrides = role_env_overrides; + merged_env_overrides.extend(role_group_env_overrides); + merged_env_overrides +} + +fn merged_cli_overrides( + role_cli_overrides: BTreeMap, + role_group_cli_overrides: BTreeMap, +) -> BTreeMap { + let mut merged_cli_overrides = role_cli_overrides; + merged_cli_overrides.extend(role_group_cli_overrides); + merged_cli_overrides +} + +fn merged_pod_overrides( + role_pod_overrides: PodTemplateSpec, + role_group_pod_overrides: PodTemplateSpec, +) -> PodTemplateSpec { + let mut merged_pod_overrides = role_group_pod_overrides; + merged_pod_overrides.merge(&role_pod_overrides); + merged_pod_overrides +} + +fn merged_product_specific_common_config(role_config: T, role_group_config: T) -> T +where + T: Merge, +{ + let mut merged_config = role_group_config; + merged_config.merge(&role_config); + merged_config +} diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 480bfd6..a994145 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -14,6 +14,15 @@ spec: nodeRoles: - cluster_manager replicas: 3 + podOverrides: + spec: + volumes: + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/scope: node,pod,service=opensearch-cluster-manager,service=opensearch-nodes-cluster-manager data: config: nodeRoles: @@ -21,3 +30,67 @@ spec: - data - remote_cluster_client replicas: 5 + podOverrides: + spec: + volumes: + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data + envOverrides: + # TODO Make these the defaults in the image + DISABLE_INSTALL_DEMO_CONFIG: "true" + OPENSEARCH_INITIAL_ADMIN_PASSWORD: super@Secret1 + configOverrides: + # TODO Add the required options to the operator + opensearch.yml: + plugins.security.allow_default_init_securityindex: "true" + # Accept the client TLS certificate generated by the + # secret-operator + # The CN matches the one of the nodes_dn and therefore + # securityadmin.sh throws an error that this is forbidden, + # but it seems to work nevertheless. + plugins.security.authcz.admin_dn.0: CN=generated certificate for pod + # Accept certificates generated by the secret-operator + plugins.security.nodes_dn: "[\"CN=generated certificate for pod\"]" + plugins.security.restapi.roles_enabled: "[\"all_access\"]" + plugins.security.ssl.transport.pemcert_filepath: /usr/share/opensearch/config/tls/tls.crt + plugins.security.ssl.transport.pemkey_filepath: /usr/share/opensearch/config/tls/tls.key + plugins.security.ssl.transport.pemtrustedcas_filepath: /usr/share/opensearch/config/tls/ca.crt + plugins.security.ssl.http.enabled: "true" + plugins.security.ssl.http.pemcert_filepath: /usr/share/opensearch/config/tls/tls.crt + plugins.security.ssl.http.pemkey_filepath: /usr/share/opensearch/config/tls/tls.key + plugins.security.ssl.http.pemtrustedcas_filepath: /usr/share/opensearch/config/tls/ca.crt + podOverrides: + spec: + containers: + - name: opensearch + volumeMounts: + - name: security-config + mountPath: /usr/share/opensearch/config/opensearch-security + readOnly: true + - name: tls + # The Java policy allows reading from /usr/share/opensearch/config. + mountPath: /usr/share/opensearch/config/tls + readOnly: true + securityContext: + fsGroup: 1000 + volumes: + - name: security-config + secret: + secretName: opensearch-security-config + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" diff --git a/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml b/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml new file mode 100644 index 0000000..b394dc5 --- /dev/null +++ b/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml @@ -0,0 +1,306 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: opensearch-security-config +stringData: + action_groups.yml: | + --- + _meta: + type: actiongroups + config_version: 2 + allowlist.yml: | + --- + _meta: + type: allowlist + config_version: 2 + + # Description: + # enabled - feature flag. + # if enabled is false, the allowlisting feature is removed. + # This is like removing the check that checks if an API is allowlisted. + # This is equivalent to continuing with the usual access control checks, and removing all the code that implements allowlisting. + # if enabled is true, then all users except SuperAdmin can access only the APIs in requests + # SuperAdmin can access all APIs. + # SuperAdmin is defined by the SuperAdmin certificate, which is configured in the opensearch.yml setting: plugins.security.authcz.admin_dn: + # Refer to the example setting in opensearch.yml.example, and the opendistro documentation to know more about configuring SuperAdmin. + # + # requests - map of allowlisted endpoints, and the allowlisted HTTP requests for those endpoints + + config: + enabled: false + audit.yml: | + --- + _meta: + type: audit + config_version: 2 + + config: + # enable/disable audit logging + enabled: true + + audit: + # Enable/disable REST API auditing + enable_rest: true + + # Categories to exclude from REST API auditing + disabled_rest_categories: + - AUTHENTICATED + - GRANTED_PRIVILEGES + + # Enable/disable Transport API auditing + enable_transport: true + + # Categories to exclude from Transport API auditing + disabled_transport_categories: + - AUTHENTICATED + - GRANTED_PRIVILEGES + + # Users to be excluded from auditing. Wildcard patterns are supported. Eg: + # ignore_users: ["test-user", "employee-*"] + ignore_users: + - kibanaserver + - xc-* + + # Requests to be excluded from auditing. Wildcard patterns are supported. Eg: + # ignore_requests: ["indices:data/read/*", "SearchRequest"] + ignore_requests: [] + + # Log individual operations in a bulk request + resolve_bulk_requests: false + + # Include the body of the request (if available) for both REST and the transport layer + log_request_body: true + + # Logs all indices affected by a request. Resolves aliases and wildcards/date patterns + resolve_indices: true + + # Exclude sensitive headers from being included in the logs. Eg: Authorization + exclude_sensitive_headers: true + + compliance: + # enable/disable compliance + enabled: true + + # Log updates to internal security changes + internal_config: true + + # Log external config files for the node + external_config: false + + # Log only metadata of the document for read events + read_metadata_only: true + + # Map of indexes and fields to monitor for read events. Wildcard patterns are supported for both index names and fields. Eg: + # read_watched_fields: { + # "twitter": ["message"] + # "logs-*": ["id", "attr*"] + # } + read_watched_fields: {} + + # List of users to ignore for read events. Wildcard patterns are supported. Eg: + # read_ignore_users: ["test-user", "employee-*"] + read_ignore_users: + - kibanaserver + + # Log only metadata of the document for write events + write_metadata_only: true + + # Log only diffs for document updates + write_log_diffs: false + + # List of indices to watch for write events. Wildcard patterns are supported + # write_watched_indices: ["twitter", "logs-*"] + write_watched_indices: [] + + # List of users to ignore for write events. Wildcard patterns are supported. Eg: + # write_ignore_users: ["test-user", "employee-*"] + write_ignore_users: + - kibanaserver + config.yml: | + --- + + # This is the main OpenSearch Security configuration file where authentication + # and authorization is defined. + # + # You need to configure at least one authentication domain in the authc of this file. + # An authentication domain is responsible for extracting the user credentials from + # the request and for validating them against an authentication backend like Active Directory for example. + # + # If more than one authentication domain is configured the first one which succeeds wins. + # If all authentication domains fail then the request is unauthenticated. + # In this case an exception is thrown and/or the HTTP status is set to 401. + # + # After authentication authorization (authz) will be applied. There can be zero or more authorizers which collect + # the roles from a given backend for the authenticated user. + # + # Both, authc and auth can be enabled/disabled separately for REST and TRANSPORT layer. Default is true for both. + # http_enabled: true + # transport_enabled: true + # + # For HTTP it is possible to allow anonymous authentication. If that is the case then the HTTP authenticators try to + # find user credentials in the HTTP request. If credentials are found then the user gets regularly authenticated. + # If none can be found the user will be authenticated as an "anonymous" user. This user has always the username "anonymous" + # and one role named "anonymous_backendrole". + # If you enable anonymous authentication all HTTP authenticators will not challenge. + # + # + # Note: If you define more than one HTTP authenticators make sure to put non-challenging authenticators like "proxy" or "clientcert" + # first and the challenging one last. + # Because it's not possible to challenge a client with two different authentication methods (for example + # Kerberos and Basic) only one can have the challenge flag set to true. You can cope with this situation + # by using pre-authentication, e.g. sending a HTTP Basic authentication header in the request. + # + # Default value of the challenge flag is true. + # + # + # HTTP + # basic (challenging) + # proxy (not challenging, needs xff) + # kerberos (challenging) + # clientcert (not challenging, needs https) + # jwt (not challenging) + # host (not challenging) #DEPRECATED, will be removed in a future version. + # host based authentication is configurable in roles_mapping + + # Authc + # internal + # noop + # ldap + + # Authz + # ldap + # noop + + _meta: + type: config + config_version: 2 + + config: + dynamic: + http: + anonymous_auth_enabled: false + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + jwt_auth_domain: + description: Authenticate via Json Web Token + http_enabled: true + transport_enabled: true + order: 0 + http_authenticator: + type: jwt + challenge: false + config: + signing_key: Uzhpcm5HOERUZ2JIbllpQVFTOEJiZ2c0Z0M1WjJRVjQ= + jwt_header: Authorization + jwt_url_parameter: null + subject_key: username + roles_key: roles + required_audience: egp + required_issuer: atruvia.de + jwt_clock_skew_tolerance_seconds: 30 + authentication_backend: + type: noop + authz: {} + internal_users.yml: | + --- + # This is the internal user database + # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh + + _meta: + type: internalusers + config_version: 2 + + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + + kibanaserver: + hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS + reserved: true + description: OpenSearch Dashboards user + + prometheusexporter: + hash: $2y$10$KuE/wuBeN4VGqHcnmL1BS.9iJESdy.avghTln10ZbA887soo4XnLK + reserved: true + description: Prometheus exporter user + nodes_dn.yml: | + --- + _meta: + type: nodesdn + config_version: 2 + roles.yml: | + --- + _meta: + type: roles + config_version: 2 + + # Allows users to monitor the cluster + monitoring: + reserved: true + cluster_permissions: + - cluster_monitor + - cluster:admin/repository/get + index_permissions: + - index_patterns: + - "*" + allowed_actions: + - indices:admin/aliases/get + - indices:admin/mappings/get + - indices:monitor/settings/get + - indices:monitor/stats + roles_mapping.yml: | + --- + # In this file users, backendroles and hosts can be mapped to Security roles. + # Permissions for OpenSearch roles are configured in roles.yml + + _meta: + type: rolesmapping + config_version: 2 + + all_access: + reserved: false + backend_roles: + - admin + description: Maps admin to all_access + + own_index: + reserved: false + users: + - "*" + description: Allow full access to an index named like the username + + monitoring: + reserved: false + users: + - prometheusexporter + + kibana_server: + reserved: true + users: + - kibanaserver + tenants.yml: | + --- + _meta: + type: tenants + config_version: 2 + whitelist.yml: | + --- + _meta: + type: whitelist + config_version: 2 + + config: + enabled: false diff --git a/tests/templates/kuttl/smoke/run-securityadmin.yaml b/tests/templates/kuttl/smoke/run-securityadmin.yaml new file mode 100644 index 0000000..b701ecd --- /dev/null +++ b/tests/templates/kuttl/smoke/run-securityadmin.yaml @@ -0,0 +1,61 @@ +# This job creates the .opendistro_security index and populates it with +# the configuration stored in the opensearch-security-config Secret. +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: run-securityadmin +spec: + template: + spec: + containers: + - name: run-securityadmin + image: opensearchproject/opensearch:3.0.0 + command: + - /bin/sh + - -c + - > + plugins/opensearch-security/tools/securityadmin.sh + -cacert config/tls-client/ca.crt + -cert config/tls-client/tls.crt + -key config/tls-client/tls.key + --hostname opensearch-nodes-cluster-manager.$NAMESPACE.svc.cluster.local + --configdir config/opensearch-security/ + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumeMounts: + - name: security-config + mountPath: /usr/share/opensearch/config/opensearch-security + readOnly: true + - name: tls-client + # The Java policy allows reading from /usr/share/opensearch/config. + mountPath: /usr/share/opensearch/config/tls-client + readOnly: true + volumes: + - name: security-config + secret: + secretName: opensearch-security-config + # Create a client TLS certificate to run securityadmin.sh + - name: tls-client + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: pod + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + # serviceAccountName: opensearch-cluster-master + securityContext: + runAsUser: 1000 + runAsGroup: 0 + fsGroup: 1000 + restartPolicy: OnFailure From c49409ef14539625fd3cc9a5c4df2b4a2e205a93 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 30 Jun 2025 16:21:06 +0200 Subject: [PATCH 15/35] Create test script --- .../kuttl/smoke/20-test-opensearch.yaml | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/templates/kuttl/smoke/20-test-opensearch.yaml diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml new file mode 100644 index 0000000..3ca48a9 --- /dev/null +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml @@ -0,0 +1,155 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-opensearch +spec: + template: + spec: + # serviceAccountName: test-sa + containers: + - name: test-opensearch + image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev + command: + - /bin/bash + - -c + args: + - | + pip install opensearch-py + python scripts/test.py + env: + # required for pip install + - name: HOME + value: /stackable + volumeMounts: + - name: script + mountPath: /stackable/scripts + - name: tls + mountPath: /stackable/tls + volumes: + - name: script + configMap: + name: test-opensearch + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsUser: 1000 + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-opensearch +data: + test.py: | + # https://docs.opensearch.org/docs/latest/clients/python-low-level/#sample-program + + from opensearchpy import OpenSearch + + # TODO Use a discovery ConfigMap + + host = 'opensearch-nodes-cluster-manager.default.svc.cluster.local' + port = 9200 + auth = ('admin', 'AJVFsGJBbpT6mChn') # For testing only. Don't store credentials in code. + ca_certs_path = '/stackable/tls/ca.crt' + + # Create the client with SSL/TLS enabled, but hostname verification disabled. + client = OpenSearch( + hosts = [{'host': host, 'port': port}], + http_compress = True, # enables gzip compression for request bodies + http_auth = auth, + # client_cert = client_cert_path, + # client_key = client_key_path, + use_ssl = True, + verify_certs = True, + ssl_assert_hostname = False, + ssl_show_warn = False, + ca_certs = ca_certs_path + ) + + # Create an index with non-default settings. + index_name = 'python-test-index' + index_body = { + 'settings': { + 'index': { + 'number_of_shards': 4 + } + } + } + + response = client.indices.create(index=index_name, body=index_body) + print('\nCreating index:') + print(response) + + # Add a document to the index. + document = { + 'title': 'Moneyball', + 'director': 'Bennett Miller', + 'year': '2011' + } + id = '1' + + response = client.index( + index = index_name, + body = document, + id = id, + refresh = True + ) + + print('\nAdding document:') + print(response) + + # Perform bulk operations + + movies = '{ "index" : { "_index" : "my-dsl-index", "_id" : "2" } } \n { "title" : "Interstellar", "director" : "Christopher Nolan", "year" : "2014"} \n { "create" : { "_index" : "my-dsl-index", "_id" : "3" } } \n { "title" : "Star Trek Beyond", "director" : "Justin Lin", "year" : "2015"} \n { "update" : {"_id" : "3", "_index" : "my-dsl-index" } } \n { "doc" : {"year" : "2016"} }' + + client.bulk(body=movies) + + # Search for the document. + q = 'miller' + query = { + 'size': 5, + 'query': { + 'multi_match': { + 'query': q, + 'fields': ['title^2', 'director'] + } + } + } + + response = client.search( + body = query, + index = index_name + ) + print('\nSearch results:') + print(response) + + # Delete the document. + response = client.delete( + index = index_name, + id = id + ) + + print('\nDeleting document:') + print(response) + + # Delete the index. + response = client.indices.delete( + index = index_name + ) + + print('\nDeleting index:') + print(response) From b4f8453a5d47ffa9cf3244580ad180c56be45b7f Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 1 Jul 2025 12:01:49 +0200 Subject: [PATCH 16/35] Fix integration test --- rust/operator-binary/src/controller/build.rs | 19 +- .../src/controller/node_config.rs | 4 +- tests/templates/kuttl/smoke/10-assert.yaml | 222 ++++++++++++++---- tests/templates/kuttl/smoke/20-assert.yaml | 11 + .../kuttl/smoke/20-test-opensearch.yaml | 2 +- 5 files changed, 208 insertions(+), 50 deletions(-) create mode 100644 tests/templates/kuttl/smoke/20-assert.yaml diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 7661790..d0b6252 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -28,11 +28,14 @@ use super::{ ContextNames, OpenSearchRoleGroupConfig, Prepared, Resources, RoleGroupName, ValidatedCluster, node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}, }; -use crate::framework::{ - IsLabelValue, RoleName, - builder::pdb::pod_disruption_budget_builder_with_role, - kvp::label::{recommended_labels, role_group_selector}, - to_qualified_role_group_name, +use crate::{ + crd::v1alpha1, + framework::{ + IsLabelValue, RoleName, + builder::pdb::pod_disruption_budget_builder_with_role, + kvp::label::{recommended_labels, role_group_selector}, + to_qualified_role_group_name, + }, }; const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; @@ -380,7 +383,11 @@ impl<'a> Builder<'a> { .with_labels(labels) .build(); - let service_selector = [("cluster-manager".to_owned(), "true".to_owned())].into(); + let service_selector = [( + v1alpha1::NodeRole::ClusterManager.to_string(), + "true".to_owned(), + )] + .into(); let service_spec = ServiceSpec { // Internal communication does not need to be exposed diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/node_config.rs index 7b5d7ce..cc11a0a 100644 --- a/rust/operator-binary/src/controller/node_config.rs +++ b/rust/operator-binary/src/controller/node_config.rs @@ -90,7 +90,7 @@ impl NodeConfig { pub fn discovery_seed_hosts(&self) -> String { // TODO Fix - "opensearch-nodes-cluster-manager.default.svc.cluster.local".to_owned() + "opensearch-cluster-manager".to_owned() } /// Configuration for `{DISCOVERY_TYPE}` @@ -166,7 +166,7 @@ impl NodeConfig { pub fn node_roles(&self, node_roles: &NodeRoles) -> String { node_roles .iter() - .map(|r| format!("{}", r)) + .map(|node_role| format!("{node_role}")) .collect::>() .join(",") } diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index 242ace8..d176bad 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -1,3 +1,10 @@ +# All fields are checked that are set by the operator. +# This helps to detect unintentional changes. +# The maintenance effort should be okay as long as it is only done in the smoke test. +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 --- apiVersion: apps/v1 kind: StatefulSet @@ -9,34 +16,101 @@ metadata: app.kubernetes.io/name: opensearch app.kubernetes.io/role-group: cluster-manager app.kubernetes.io/version: 3.0.0 - cluster-manager: "true" stackable.tech/vendor: Stackable name: opensearch-nodes-cluster-manager + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch spec: podManagementPolicy: Parallel replicas: 3 + selector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + serviceName: "" template: + metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + cluster_manager: "true" + stackable.tech/vendor: Stackable spec: containers: - - name: opensearch - env: - - name: discovery.seed_hosts - value: opensearch-cluster-manager - - name: discovery.type - value: zen - - name: cluster.initial_cluster_manager_nodes - value: opensearch-nodes-cluster-manager-0,opensearch-nodes-cluster-manager-1,opensearch-nodes-cluster-manager-2 - - name: network.host - value: 0.0.0.0 - - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD - value: super@Secret1 - - name: node.name - valueFrom: - fieldRef: - apiVersion: v1 - fieldPath: metadata.name - - name: node.roles - value: cluster-manager + - command: + - /usr/share/opensearch/opensearch-docker-entrypoint.sh + env: + - name: DISABLE_INSTALL_DEMO_CONFIG + value: "true" + - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD + value: super@Secret1 + - name: cluster.initial_cluster_manager_nodes + value: opensearch-nodes-cluster-manager-0,opensearch-nodes-cluster-manager-1,opensearch-nodes-cluster-manager-2 + - name: discovery.seed_hosts + value: opensearch-cluster-manager + - name: node.name + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: node.roles + value: cluster_manager + image: opensearchproject/opensearch:3.0.0 + imagePullPolicy: Always + name: opensearch + ports: + - containerPort: 9200 + name: http + protocol: TCP + - containerPort: 9300 + name: transport + protocol: TCP + volumeMounts: + - mountPath: /usr/share/opensearch/config/opensearch.yml + name: config + readOnly: true + subPath: opensearch.yml + - mountPath: /usr/share/opensearch/config/opensearch-security + name: security-config + readOnly: true + - mountPath: /usr/share/opensearch/config/tls + name: tls + readOnly: true + securityContext: + fsGroup: 1000 + volumes: + - configMap: + name: opensearch-nodes-cluster-manager + name: config + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: node,pod,service=opensearch-cluster-manager,service=opensearch-nodes-cluster-manager + creationTimestamp: null + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls + - name: security-config + secret: + defaultMode: 420 + secretName: opensearch-security-config status: readyReplicas: 3 replicas: 3 @@ -51,35 +125,101 @@ metadata: app.kubernetes.io/name: opensearch app.kubernetes.io/role-group: data app.kubernetes.io/version: 3.0.0 - data: "true" - ingest: "true" - remote-cluster-client: "true" stackable.tech/vendor: Stackable name: opensearch-nodes-data + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch spec: podManagementPolicy: Parallel replicas: 5 + selector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + serviceName: "" template: + metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + data: "true" + ingest: "true" + remote_cluster_client: "true" + stackable.tech/vendor: Stackable spec: containers: - - name: opensearch - env: - - name: discovery.seed_hosts - value: opensearch-cluster-manager - - name: discovery.type - value: zen - - name: cluster.initial_cluster_manager_nodes - - name: network.host - value: 0.0.0.0 - - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD - value: super@Secret1 - - name: node.name - valueFrom: - fieldRef: - apiVersion: v1 - fieldPath: metadata.name - - name: node.roles - value: ingest,data,remote-cluster-client + - command: + - /usr/share/opensearch/opensearch-docker-entrypoint.sh + env: + - name: DISABLE_INSTALL_DEMO_CONFIG + value: "true" + - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD + value: super@Secret1 + - name: cluster.initial_cluster_manager_nodes + - name: discovery.seed_hosts + value: opensearch-cluster-manager + - name: node.name + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: node.roles + value: ingest,data,remote_cluster_client + image: opensearchproject/opensearch:3.0.0 + imagePullPolicy: Always + name: opensearch + ports: + - containerPort: 9200 + name: http + protocol: TCP + - containerPort: 9300 + name: transport + protocol: TCP + volumeMounts: + - mountPath: /usr/share/opensearch/config/opensearch.yml + name: config + readOnly: true + subPath: opensearch.yml + - mountPath: /usr/share/opensearch/config/opensearch-security + name: security-config + readOnly: true + - mountPath: /usr/share/opensearch/config/tls + name: tls + readOnly: true + securityContext: + fsGroup: 1000 + volumes: + - configMap: + name: opensearch-nodes-data + name: config + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data + creationTimestamp: null + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls + - name: security-config + secret: + secretName: opensearch-security-config status: readyReplicas: 5 replicas: 5 diff --git a/tests/templates/kuttl/smoke/20-assert.yaml b/tests/templates/kuttl/smoke/20-assert.yaml new file mode 100644 index 0000000..bebbaa9 --- /dev/null +++ b/tests/templates/kuttl/smoke/20-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-opensearch +status: + succeeded: 1 diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml index 3ca48a9..4d9fcff 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml @@ -61,7 +61,7 @@ data: # TODO Use a discovery ConfigMap - host = 'opensearch-nodes-cluster-manager.default.svc.cluster.local' + host = 'opensearch-cluster-manager' port = 9200 auth = ('admin', 'AJVFsGJBbpT6mChn') # For testing only. Don't store credentials in code. ca_certs_path = '/stackable/tls/ca.crt' From 7f1e77792529434eb2b26e36ddf2593446ca1bbc Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 1 Jul 2025 12:38:27 +0200 Subject: [PATCH 17/35] test(smoke): Improve security config --- .../kuttl/smoke/10-install-opensearch.yaml | 97 ++++++ .../smoke/10-opensearch-security-config.yaml | 306 ------------------ 2 files changed, 97 insertions(+), 306 deletions(-) delete mode 100644 tests/templates/kuttl/smoke/10-opensearch-security-config.yaml diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index a994145..1ef0737 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -94,3 +94,100 @@ spec: resources: requests: storage: "1" +--- +apiVersion: v1 +kind: Secret +metadata: + name: opensearch-security-config +stringData: + action_groups.yml: | + --- + _meta: + type: actiongroups + config_version: 2 + allowlist.yml: | + --- + _meta: + type: allowlist + config_version: 2 + + config: + enabled: false + audit.yml: | + --- + _meta: + type: audit + config_version: 2 + + config: + enabled: false + config.yml: | + --- + _meta: + type: config + config_version: 2 + + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internal_users.yml: | + --- + # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh + + _meta: + type: internalusers + config_version: 2 + + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + + kibanaserver: + hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS + reserved: true + description: OpenSearch Dashboards user + nodes_dn.yml: | + --- + _meta: + type: nodesdn + config_version: 2 + roles.yml: | + --- + _meta: + type: roles + config_version: 2 + roles_mapping.yml: | + --- + _meta: + type: rolesmapping + config_version: 2 + + all_access: + reserved: false + backend_roles: + - admin + description: Maps admin to all_access + + kibana_server: + reserved: true + users: + - kibanaserver + tenants.yml: | + --- + _meta: + type: tenants + config_version: 2 diff --git a/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml b/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml deleted file mode 100644 index b394dc5..0000000 --- a/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml +++ /dev/null @@ -1,306 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-security-config -stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - # Description: - # enabled - feature flag. - # if enabled is false, the allowlisting feature is removed. - # This is like removing the check that checks if an API is allowlisted. - # This is equivalent to continuing with the usual access control checks, and removing all the code that implements allowlisting. - # if enabled is true, then all users except SuperAdmin can access only the APIs in requests - # SuperAdmin can access all APIs. - # SuperAdmin is defined by the SuperAdmin certificate, which is configured in the opensearch.yml setting: plugins.security.authcz.admin_dn: - # Refer to the example setting in opensearch.yml.example, and the opendistro documentation to know more about configuring SuperAdmin. - # - # requests - map of allowlisted endpoints, and the allowlisted HTTP requests for those endpoints - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - # enable/disable audit logging - enabled: true - - audit: - # Enable/disable REST API auditing - enable_rest: true - - # Categories to exclude from REST API auditing - disabled_rest_categories: - - AUTHENTICATED - - GRANTED_PRIVILEGES - - # Enable/disable Transport API auditing - enable_transport: true - - # Categories to exclude from Transport API auditing - disabled_transport_categories: - - AUTHENTICATED - - GRANTED_PRIVILEGES - - # Users to be excluded from auditing. Wildcard patterns are supported. Eg: - # ignore_users: ["test-user", "employee-*"] - ignore_users: - - kibanaserver - - xc-* - - # Requests to be excluded from auditing. Wildcard patterns are supported. Eg: - # ignore_requests: ["indices:data/read/*", "SearchRequest"] - ignore_requests: [] - - # Log individual operations in a bulk request - resolve_bulk_requests: false - - # Include the body of the request (if available) for both REST and the transport layer - log_request_body: true - - # Logs all indices affected by a request. Resolves aliases and wildcards/date patterns - resolve_indices: true - - # Exclude sensitive headers from being included in the logs. Eg: Authorization - exclude_sensitive_headers: true - - compliance: - # enable/disable compliance - enabled: true - - # Log updates to internal security changes - internal_config: true - - # Log external config files for the node - external_config: false - - # Log only metadata of the document for read events - read_metadata_only: true - - # Map of indexes and fields to monitor for read events. Wildcard patterns are supported for both index names and fields. Eg: - # read_watched_fields: { - # "twitter": ["message"] - # "logs-*": ["id", "attr*"] - # } - read_watched_fields: {} - - # List of users to ignore for read events. Wildcard patterns are supported. Eg: - # read_ignore_users: ["test-user", "employee-*"] - read_ignore_users: - - kibanaserver - - # Log only metadata of the document for write events - write_metadata_only: true - - # Log only diffs for document updates - write_log_diffs: false - - # List of indices to watch for write events. Wildcard patterns are supported - # write_watched_indices: ["twitter", "logs-*"] - write_watched_indices: [] - - # List of users to ignore for write events. Wildcard patterns are supported. Eg: - # write_ignore_users: ["test-user", "employee-*"] - write_ignore_users: - - kibanaserver - config.yml: | - --- - - # This is the main OpenSearch Security configuration file where authentication - # and authorization is defined. - # - # You need to configure at least one authentication domain in the authc of this file. - # An authentication domain is responsible for extracting the user credentials from - # the request and for validating them against an authentication backend like Active Directory for example. - # - # If more than one authentication domain is configured the first one which succeeds wins. - # If all authentication domains fail then the request is unauthenticated. - # In this case an exception is thrown and/or the HTTP status is set to 401. - # - # After authentication authorization (authz) will be applied. There can be zero or more authorizers which collect - # the roles from a given backend for the authenticated user. - # - # Both, authc and auth can be enabled/disabled separately for REST and TRANSPORT layer. Default is true for both. - # http_enabled: true - # transport_enabled: true - # - # For HTTP it is possible to allow anonymous authentication. If that is the case then the HTTP authenticators try to - # find user credentials in the HTTP request. If credentials are found then the user gets regularly authenticated. - # If none can be found the user will be authenticated as an "anonymous" user. This user has always the username "anonymous" - # and one role named "anonymous_backendrole". - # If you enable anonymous authentication all HTTP authenticators will not challenge. - # - # - # Note: If you define more than one HTTP authenticators make sure to put non-challenging authenticators like "proxy" or "clientcert" - # first and the challenging one last. - # Because it's not possible to challenge a client with two different authentication methods (for example - # Kerberos and Basic) only one can have the challenge flag set to true. You can cope with this situation - # by using pre-authentication, e.g. sending a HTTP Basic authentication header in the request. - # - # Default value of the challenge flag is true. - # - # - # HTTP - # basic (challenging) - # proxy (not challenging, needs xff) - # kerberos (challenging) - # clientcert (not challenging, needs https) - # jwt (not challenging) - # host (not challenging) #DEPRECATED, will be removed in a future version. - # host based authentication is configurable in roles_mapping - - # Authc - # internal - # noop - # ldap - - # Authz - # ldap - # noop - - _meta: - type: config - config_version: 2 - - config: - dynamic: - http: - anonymous_auth_enabled: false - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - jwt_auth_domain: - description: Authenticate via Json Web Token - http_enabled: true - transport_enabled: true - order: 0 - http_authenticator: - type: jwt - challenge: false - config: - signing_key: Uzhpcm5HOERUZ2JIbllpQVFTOEJiZ2c0Z0M1WjJRVjQ= - jwt_header: Authorization - jwt_url_parameter: null - subject_key: username - roles_key: roles - required_audience: egp - required_issuer: atruvia.de - jwt_clock_skew_tolerance_seconds: 30 - authentication_backend: - type: noop - authz: {} - internal_users.yml: | - --- - # This is the internal user database - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - - _meta: - type: internalusers - config_version: 2 - - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - - prometheusexporter: - hash: $2y$10$KuE/wuBeN4VGqHcnmL1BS.9iJESdy.avghTln10ZbA887soo4XnLK - reserved: true - description: Prometheus exporter user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - - # Allows users to monitor the cluster - monitoring: - reserved: true - cluster_permissions: - - cluster_monitor - - cluster:admin/repository/get - index_permissions: - - index_patterns: - - "*" - allowed_actions: - - indices:admin/aliases/get - - indices:admin/mappings/get - - indices:monitor/settings/get - - indices:monitor/stats - roles_mapping.yml: | - --- - # In this file users, backendroles and hosts can be mapped to Security roles. - # Permissions for OpenSearch roles are configured in roles.yml - - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - description: Maps admin to all_access - - own_index: - reserved: false - users: - - "*" - description: Allow full access to an index named like the username - - monitoring: - reserved: false - users: - - prometheusexporter - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 - whitelist.yml: | - --- - _meta: - type: whitelist - config_version: 2 - - config: - enabled: false From e1d8bcf3dc0871788d6a5680dca91393301814c9 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 1 Jul 2025 13:23:58 +0200 Subject: [PATCH 18/35] Add startup and readiness probes --- rust/operator-binary/src/controller/build.rs | 31 ++++++++++++++++++-- tests/templates/kuttl/smoke/10-assert.yaml | 30 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index d0b6252..2dc6f42 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -11,12 +11,12 @@ use stackable_operator::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ - ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodTemplateSpec, - Service, ServicePort, ServiceSpec, Volume, VolumeMount, + ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodTemplateSpec, Probe, + Service, ServicePort, ServiceSpec, TCPSocketAction, Volume, VolumeMount, }, policy::v1::PodDisruptionBudget, }, - apimachinery::pkg::apis::meta::v1::LabelSelector, + apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, }, kvp::{ Label, Labels, ObjectLabels, @@ -223,6 +223,29 @@ impl<'a> Builder<'a> { .image .resolve("opensearch", crate::built_info::PKG_VERSION); + // Probe values taken from the official Helm chart + let startup_probe = Probe { + failure_threshold: Some(30), + initial_delay_seconds: Some(5), + period_seconds: Some(10), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::Int(9200), + ..TCPSocketAction::default() + }), + timeout_seconds: Some(3), + ..Probe::default() + }; + let readiness_probe = Probe { + failure_threshold: Some(3), + period_seconds: Some(5), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::Int(9200), + ..TCPSocketAction::default() + }), + timeout_seconds: Some(3), + ..Probe::default() + }; + ContainerBuilder::new("opensearch") .expect("should be a valid container name") .image_from_product_image(&product_image) @@ -257,6 +280,8 @@ impl<'a> Builder<'a> { ..ContainerPort::default() }, ]) + .startup_probe(startup_probe) + .readiness_probe(readiness_probe) .build() } diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index d176bad..8b2ef2b 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -74,6 +74,21 @@ spec: - containerPort: 9300 name: transport protocol: TCP + readinessProbe: + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 9200 + timeoutSeconds: 3 + startupProbe: + failureThreshold: 30 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: 9200 + timeoutSeconds: 3 volumeMounts: - mountPath: /usr/share/opensearch/config/opensearch.yml name: config @@ -184,6 +199,21 @@ spec: - containerPort: 9300 name: transport protocol: TCP + readinessProbe: + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 9200 + timeoutSeconds: 3 + startupProbe: + failureThreshold: 30 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: 9200 + timeoutSeconds: 3 volumeMounts: - mountPath: /usr/share/opensearch/config/opensearch.yml name: config From 7a2b6d9769a13d6a646a385068ac28deaba4ef75 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 1 Jul 2025 18:10:44 +0200 Subject: [PATCH 19/35] Restructure role and role-group builder --- rust/operator-binary/src/controller.rs | 4 +- rust/operator-binary/src/controller/build.rs | 490 ++++++++++-------- .../src/controller/node_config.rs | 4 +- .../src/controller/validate.rs | 2 +- rust/operator-binary/src/framework/builder.rs | 1 + .../src/framework/builder/meta.rs | 23 + .../src/framework/kvp/label.rs | 2 + tests/templates/kuttl/smoke/10-assert.yaml | 14 +- .../kuttl/smoke/10-install-opensearch.yaml | 1 - .../kuttl/smoke/run-securityadmin.yaml | 61 --- 10 files changed, 303 insertions(+), 299 deletions(-) create mode 100644 rust/operator-binary/src/framework/builder/meta.rs delete mode 100644 tests/templates/kuttl/smoke/run-securityadmin.yaml diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 830df49..97cc58b 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc}; use apply::Applier; -use build::Builder; +use build::build; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, @@ -227,7 +227,7 @@ pub async fn reconcile( let validated_cluster = validate(cluster).context(ValidateClusterSnafu)?; // build (no client required; infallible) - let prepared_resources = Builder::new(&context.names, validated_cluster.clone()).build(); + let prepared_resources = build(&context.names, validated_cluster.clone()); // apply (client required) let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 2dc6f42..20f26b9 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -2,7 +2,6 @@ use std::{marker::PhantomData, str::FromStr}; use stackable_operator::{ builder::{ - configmap::ConfigMapBuilder, meta::ObjectMetaBuilder, pod::{PodBuilder, container::ContainerBuilder}, }, @@ -19,7 +18,7 @@ use stackable_operator::{ apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, }, kvp::{ - Label, Labels, ObjectLabels, + Label, Labels, consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, }, }; @@ -31,135 +30,244 @@ use super::{ use crate::{ crd::v1alpha1, framework::{ - IsLabelValue, RoleName, - builder::pdb::pod_disruption_budget_builder_with_role, + RoleName, + builder::{ + meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, + }, kvp::label::{recommended_labels, role_group_selector}, to_qualified_role_group_name, }, }; const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; - -// TODO Convert to RoleGroupBuilder -pub struct Builder<'a> { +const CONFIG_VOLUME_NAME: &str = "config"; +const HTTP_PORT_NAME: &str = "http"; +const HTTP_PORT: u16 = 9200; +const TRANSPORT_PORT_NAME: &str = "transport"; +const TRANSPORT_PORT: u16 = 9300; +const METRICS_PORT_NAME: &str = "metrics"; +const METRICS_PORT: u16 = 9600; + +struct RoleBuilder<'a> { names: &'a ContextNames, role_name: RoleName, cluster: ValidatedCluster, - node_config: NodeConfig, } -impl<'a> Builder<'a> { - pub fn new(names: &'a ContextNames, cluster: ValidatedCluster) -> Builder<'a> { - let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); - Builder { +impl<'a> RoleBuilder<'a> { + fn new( + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, + ) -> RoleBuilder<'a> { + RoleBuilder { names, role_name: role_name.clone(), cluster: cluster.clone(), - node_config: NodeConfig::new(role_name, cluster), } } - pub fn build(&self) -> Resources { - let mut config_maps = vec![]; - let mut stateful_sets = vec![]; - let mut services = vec![]; - - for (role_group_name, role_group_config) in &self.cluster.role_group_configs { - // used for the name of the StatefulSet, role-group ConfigMap, ... - let qualified_role_group_name = - to_qualified_role_group_name(&self.cluster.name, &self.role_name, role_group_name); - - let config_map = self.build_role_group_config_map( - &qualified_role_group_name, - role_group_name, - role_group_config, - ); - let stateful_set = self.build_statefulset( - &qualified_role_group_name, - role_group_name, - role_group_config, - ); - - let service = - self.build_role_group_service(&qualified_role_group_name, role_group_name); - - config_maps.push(config_map); - stateful_sets.push(stateful_set); - services.push(service); + fn role_group_builders(&self) -> Vec { + self.cluster + .role_group_configs + .iter() + .map(|(role_group_name, role_group_config)| { + RoleGroupBuilder::new( + self.names, + self.role_name.clone(), + self.cluster.clone(), + role_group_name.clone(), + role_group_config.clone(), + ) + }) + .collect() + } + + fn build_cluster_manager_service(&self) -> Service { + // TODO Share port config + let ports = vec![ + ServicePort { + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(METRICS_PORT_NAME.to_owned()), + port: METRICS_PORT.into(), + ..ServicePort::default() + }, + ]; + + // Well-known Kubernetes labels + let mut labels = Labels::role_selector( + &self.cluster, + &self.names.product_name.to_string(), + &self.role_name.to_string(), + ) + .unwrap(); + + let managed_by = Label::managed_by( + &self.names.operator_name.to_string(), + &self.names.controller_name.to_string(), + ) + .unwrap(); + let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); + + labels.insert(managed_by); + labels.insert(version); + + // Stackable-specific labels + labels + .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) + .unwrap(); + + let metadata = ObjectMetaBuilder::new() + .name(format!("{}-cluster-manager", self.cluster.name)) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(labels) + .build(); + + let service_selector = [( + v1alpha1::NodeRole::ClusterManager.to_string(), + "true".to_owned(), + )] + .into(); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(service_selector), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, } + } - let cluster_manager_service = self.build_cluster_manager_service(); - services.push(cluster_manager_service); + fn build_pdb(&self) -> Option { + let pdb_config = &self.cluster.role_config.pod_disruption_budget; - let pod_disruption_budgets = self.build_pdb().into_iter().collect(); + if pdb_config.enabled { + let max_unavailable = pdb_config + .max_unavailable + .unwrap_or(PDB_DEFAULT_MAX_UNAVAILABLE); + Some( + pod_disruption_budget_builder_with_role( + &self.cluster, + &self.names.product_name, + &self.role_name, + &self.names.operator_name, + &self.names.controller_name, + ) + .with_max_unavailable(max_unavailable) + .build(), + ) + } else { + None + } + } +} - Resources { - stateful_sets, - services, - config_maps, - pod_disruption_budgets, - status: PhantomData, +struct RoleGroupBuilder<'a> { + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, + node_config: NodeConfig, + qualified_role_group_name: String, + role_group_name: RoleGroupName, + role_group_config: OpenSearchRoleGroupConfig, +} + +impl<'a> RoleGroupBuilder<'a> { + fn new( + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, + role_group_name: RoleGroupName, + role_group_config: OpenSearchRoleGroupConfig, + ) -> RoleGroupBuilder<'a> { + // used for the name of the StatefulSet, role-group ConfigMap, ... + let qualified_role_group_name = + to_qualified_role_group_name(&cluster.name, &role_name, &role_group_name); + + RoleGroupBuilder { + names, + role_name: role_name.clone(), + cluster: cluster.clone(), + node_config: NodeConfig::new(role_name, cluster), + qualified_role_group_name, + role_group_name, + role_group_config, } } - fn build_role_group_config_map( - &self, - config_map_name: &str, - role_group_name: &RoleGroupName, - role_group_config: &OpenSearchRoleGroupConfig, - ) -> ConfigMap { + fn build_config_map(&self) -> ConfigMap { let metadata = ObjectMetaBuilder::new() - .name(config_map_name) + .name(self.qualified_role_group_name.clone()) .namespace(&self.cluster.namespace) - .ownerreference_from_resource(&self.cluster, None, Some(true)) - // TODO Fix - .unwrap() - .with_labels(self.build_recommended_labels(role_group_name)) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.build_recommended_labels()) .build(); - ConfigMapBuilder::new() - .metadata(metadata) - .add_data( - CONFIGURATION_FILE_OPENSEARCH_YML, - self.node_config.static_opensearch_config(role_group_config), - ) - .build() - // TODO Fix - .unwrap() + let data = [( + CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), + self.node_config + .static_opensearch_config(&self.role_group_config), + )] + .into(); + + ConfigMap { + metadata, + data: Some(data), + ..ConfigMap::default() + } } - fn build_statefulset( - &self, - qualified_role_group_name: &str, - role_group_name: &RoleGroupName, - role_group_config: &OpenSearchRoleGroupConfig, - ) -> StatefulSet { + fn build_statefulset(&self) -> StatefulSet { let metadata = ObjectMetaBuilder::new() - .name(qualified_role_group_name) + .name(self.qualified_role_group_name.clone()) .namespace(&self.cluster.namespace) - .ownerreference_from_resource(&self.cluster, None, Some(true)) - // TODO Fix - .unwrap() - .with_labels(self.build_recommended_labels(role_group_name)) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.build_recommended_labels()) .build(); - let template = self.build_pod_template( - qualified_role_group_name, - role_group_name, - role_group_config, - ); + let template = self.build_pod_template(); let statefulset_match_labels = role_group_selector( &self.cluster, &self.names.product_name, &self.role_name, - role_group_name, + &self.role_group_name, ); let spec = StatefulSetSpec { // Order does not matter for OpenSearch pod_management_policy: Some("Parallel".to_string()), - replicas: Some(role_group_config.replicas as i32), + replicas: Some(self.role_group_config.replicas.into()), selector: LabelSelector { match_labels: Some(statefulset_match_labels.into()), ..LabelSelector::default() @@ -176,43 +284,37 @@ impl<'a> Builder<'a> { } } - fn build_pod_template( - &self, - qualified_role_group_name: &str, - role_group_name: &RoleGroupName, - role_group_config: &OpenSearchRoleGroupConfig, - ) -> PodTemplateSpec { + fn build_pod_template(&self) -> PodTemplateSpec { let mut builder = PodBuilder::new(); let mut node_role_labels = Labels::new(); - for node_role in role_group_config.config.node_roles.iter() { + for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels .insert(Label::try_from((format!("{node_role}"), "true".to_string())).unwrap()); } let metadata = ObjectMetaBuilder::new() - .with_labels(self.build_recommended_labels(role_group_name)) + .with_labels(self.build_recommended_labels()) .with_labels(node_role_labels) .build(); - let container = self.build_container(role_group_config); + let container = self.build_container(&self.role_group_config); let mut pod_template = builder .metadata(metadata) .add_container(container) .add_volume(Volume { - name: "config".to_string(), + name: CONFIG_VOLUME_NAME.to_owned(), config_map: Some(ConfigMapVolumeSource { - name: qualified_role_group_name.to_owned(), + name: self.qualified_role_group_name.clone(), ..Default::default() }), ..Default::default() }) - // TODO ? - .unwrap() + .expect("The volume names are statically defined and there should be no duplicates.") .build_template(); - pod_template.merge_from(role_group_config.pod_overrides.clone()); + pod_template.merge_from(self.role_group_config.pod_overrides.clone()); pod_template } @@ -229,7 +331,7 @@ impl<'a> Builder<'a> { initial_delay_seconds: Some(5), period_seconds: Some(10), tcp_socket: Some(TCPSocketAction { - port: IntOrString::Int(9200), + port: IntOrString::String(HTTP_PORT_NAME.to_owned()), ..TCPSocketAction::default() }), timeout_seconds: Some(3), @@ -239,7 +341,7 @@ impl<'a> Builder<'a> { failure_threshold: Some(3), period_seconds: Some(5), tcp_socket: Some(TCPSocketAction { - port: IntOrString::Int(9200), + port: IntOrString::String(HTTP_PORT_NAME.to_owned()), ..TCPSocketAction::default() }), timeout_seconds: Some(3), @@ -261,22 +363,26 @@ impl<'a> Builder<'a> { .add_volume_mounts([VolumeMount { // TODO Use path and file constants mount_path: "/usr/share/opensearch/config/opensearch.yml".to_owned(), - name: "config".to_owned(), + name: CONFIG_VOLUME_NAME.to_owned(), read_only: Some(true), - sub_path: Some("opensearch.yml".to_owned()), + sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), ..VolumeMount::default() }]) - // TODO ? - .unwrap() + .expect("The mount paths are statically defined and there should be no duplicates.") .add_container_ports(vec![ ContainerPort { - name: Some("http".to_owned()), - container_port: 9200, + name: Some(HTTP_PORT_NAME.to_owned()), + container_port: HTTP_PORT.into(), ..ContainerPort::default() }, ContainerPort { - name: Some("transport".to_owned()), - container_port: 9300, + name: Some(TRANSPORT_PORT_NAME.to_owned()), + container_port: TRANSPORT_PORT.into(), + ..ContainerPort::default() + }, + ContainerPort { + name: Some(METRICS_PORT_NAME.to_owned()), + container_port: METRICS_PORT.into(), ..ContainerPort::default() }, ]) @@ -285,7 +391,7 @@ impl<'a> Builder<'a> { .build() } - fn build_recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { + fn build_recommended_labels(&self) -> Labels { recommended_labels( &self.cluster, &self.names.product_name, @@ -293,24 +399,25 @@ impl<'a> Builder<'a> { &self.names.operator_name, &self.names.controller_name, &self.role_name, - role_group_name, + &self.role_group_name, ) } - fn build_role_group_service( - &self, - qualified_role_group_name: &str, - role_group_name: &RoleGroupName, - ) -> Service { + fn build_service(&self) -> Service { let ports = vec![ ServicePort { - name: Some("http".to_owned()), - port: 9200, + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), ..ServicePort::default() }, ServicePort { - name: Some("transport".to_owned()), - port: 9300, + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(METRICS_PORT_NAME.to_owned()), + port: METRICS_PORT.into(), ..ServicePort::default() }, ]; @@ -318,32 +425,22 @@ impl<'a> Builder<'a> { // TODO Add metrics port and Prometheus label let metadata = ObjectMetaBuilder::new() - .name(qualified_role_group_name) + .name(self.qualified_role_group_name.clone()) .namespace(&self.cluster.namespace) - .ownerreference_from_resource(&self.cluster, None, Some(true)) - // TODO Fix - .unwrap() - .with_recommended_labels(ObjectLabels { - owner: &self.cluster, - app_name: &self.names.product_name.to_label_value(), - app_version: &self.cluster.product_version.to_label_value(), - operator_name: &self.names.operator_name.to_label_value(), - controller_name: &self.names.controller_name.to_label_value(), - role: &self.role_name.to_label_value(), - role_group: &role_group_name.to_label_value(), - }) - // TODO fix - .unwrap() + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.build_recommended_labels()) .build(); - let service_selector = Labels::role_group_selector( + let service_selector = role_group_selector( &self.cluster, - &self.names.product_name.to_label_value(), - &self.role_name.to_label_value(), - &role_group_name.to_label_value(), - ) - // TODO fix - .unwrap(); + &self.names.product_name, + &self.role_name, + &self.role_group_name, + ); let service_spec = ServiceSpec { // Internal communication does not need to be exposed @@ -361,96 +458,33 @@ impl<'a> Builder<'a> { status: None, } } +} - fn build_cluster_manager_service(&self) -> Service { - let ports = vec![ - ServicePort { - name: Some("http".to_owned()), - port: 9200, - ..ServicePort::default() - }, - ServicePort { - name: Some("transport".to_owned()), - port: 9300, - ..ServicePort::default() - }, - ]; - - // Well-known Kubernetes labels - let mut labels = Labels::role_selector( - &self.cluster, - &self.names.product_name.to_string(), - &self.role_name.to_string(), - ) - .unwrap(); - - let managed_by = Label::managed_by( - &self.names.operator_name.to_string(), - &self.names.controller_name.to_string(), - ) - .unwrap(); - let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); - - labels.insert(managed_by); - labels.insert(version); - - // Stackable-specific labels - labels - .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) - .unwrap(); +pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> Resources { + let mut config_maps = vec![]; + let mut stateful_sets = vec![]; + let mut services = vec![]; - let metadata = ObjectMetaBuilder::new() - .name(format!("{}-cluster-manager", self.cluster.name)) - .namespace(&self.cluster.namespace) - .ownerreference_from_resource(&self.cluster, None, Some(true)) - // TODO Fix - .unwrap() - .with_labels(labels) - .build(); - - let service_selector = [( - v1alpha1::NodeRole::ClusterManager.to_string(), - "true".to_owned(), - )] - .into(); + let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); - let service_spec = ServiceSpec { - // Internal communication does not need to be exposed - type_: Some("ClusterIP".to_string()), - cluster_ip: Some("None".to_string()), - ports: Some(ports), - selector: Some(service_selector), - publish_not_ready_addresses: Some(true), - ..ServiceSpec::default() - }; + let role_builder = RoleBuilder::new(names, role_name, cluster.clone()); - Service { - metadata, - spec: Some(service_spec), - status: None, - } + for role_group_builder in role_builder.role_group_builders() { + config_maps.push(role_group_builder.build_config_map()); + stateful_sets.push(role_group_builder.build_statefulset()); + services.push(role_group_builder.build_service()); } - fn build_pdb(&self) -> Option { - let pdb_config = &self.cluster.role_config.pod_disruption_budget; + let cluster_manager_service = role_builder.build_cluster_manager_service(); + services.push(cluster_manager_service); - if pdb_config.enabled { - let max_unavailable = pdb_config - .max_unavailable - .unwrap_or(PDB_DEFAULT_MAX_UNAVAILABLE); - Some( - pod_disruption_budget_builder_with_role( - &self.cluster, - &self.names.product_name, - &self.role_name, - &self.names.operator_name, - &self.names.controller_name, - ) - .with_max_unavailable(max_unavailable) - .build(), - ) - } else { - None - } + let pod_disruption_budgets = role_builder.build_pdb().into_iter().collect(); + + Resources { + stateful_sets, + services, + config_maps, + pod_disruption_budgets, + status: PhantomData, } } diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/node_config.rs index cc11a0a..1d725c5 100644 --- a/rust/operator-binary/src/controller/node_config.rs +++ b/rust/operator-binary/src/controller/node_config.rs @@ -89,8 +89,8 @@ impl NodeConfig { } pub fn discovery_seed_hosts(&self) -> String { - // TODO Fix - "opensearch-cluster-manager".to_owned() + // TODO Check length + format!("{}-cluster-manager", self.cluster_name()) } /// Configuration for `{DISCOVERY_TYPE}` diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index a2d9596..6d154de 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -68,7 +68,7 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result + HasUid), + block_owner_deletion: Option, + controller: Option, +) -> OwnerReference { + OwnerReferenceBuilder::new() + // Set api_version, kind, name and additionally the UID if it exists. + .initialize_from_resource(resource) + // Ensure that the UID is set. + .uid(resource.to_uid()) + .block_owner_deletion_opt(block_owner_deletion) + .controller_opt(controller) + .build() + .expect("api_version, kind, name and uid should be set") +} diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs index 48b79af..ec96661 100644 --- a/rust/operator-binary/src/framework/kvp/label.rs +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -10,6 +10,7 @@ use crate::framework::{ pub const LABEL_VALUE_MAX_LENGTH: usize = 63; +/// Infallible variant of `Labels::recommended` pub fn recommended_labels( owner: &(impl Resource + IsLabelValue), product_name: &ProductName, @@ -32,6 +33,7 @@ pub fn recommended_labels( .expect("Labels should be created because all given parameters produce valid label values") } +/// Infallible variant of `Labels::role_group_selector` pub fn role_group_selector( owner: &(impl Resource + IsLabelValue), product_name: &ProductName, diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index 8b2ef2b..812cddb 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -74,12 +74,15 @@ spec: - containerPort: 9300 name: transport protocol: TCP + - containerPort: 9600 + name: metrics + protocol: TCP readinessProbe: failureThreshold: 3 periodSeconds: 5 successThreshold: 1 tcpSocket: - port: 9200 + port: http timeoutSeconds: 3 startupProbe: failureThreshold: 30 @@ -87,7 +90,7 @@ spec: periodSeconds: 10 successThreshold: 1 tcpSocket: - port: 9200 + port: http timeoutSeconds: 3 volumeMounts: - mountPath: /usr/share/opensearch/config/opensearch.yml @@ -199,12 +202,15 @@ spec: - containerPort: 9300 name: transport protocol: TCP + - containerPort: 9600 + name: metrics + protocol: TCP readinessProbe: failureThreshold: 3 periodSeconds: 5 successThreshold: 1 tcpSocket: - port: 9200 + port: http timeoutSeconds: 3 startupProbe: failureThreshold: 30 @@ -212,7 +218,7 @@ spec: periodSeconds: 10 successThreshold: 1 tcpSocket: - port: 9200 + port: http timeoutSeconds: 3 volumeMounts: - mountPath: /usr/share/opensearch/config/opensearch.yml diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 1ef0737..7df5a40 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -180,7 +180,6 @@ stringData: reserved: false backend_roles: - admin - description: Maps admin to all_access kibana_server: reserved: true diff --git a/tests/templates/kuttl/smoke/run-securityadmin.yaml b/tests/templates/kuttl/smoke/run-securityadmin.yaml deleted file mode 100644 index b701ecd..0000000 --- a/tests/templates/kuttl/smoke/run-securityadmin.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# This job creates the .opendistro_security index and populates it with -# the configuration stored in the opensearch-security-config Secret. ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: run-securityadmin -spec: - template: - spec: - containers: - - name: run-securityadmin - image: opensearchproject/opensearch:3.0.0 - command: - - /bin/sh - - -c - - > - plugins/opensearch-security/tools/securityadmin.sh - -cacert config/tls-client/ca.crt - -cert config/tls-client/tls.crt - -key config/tls-client/tls.key - --hostname opensearch-nodes-cluster-manager.$NAMESPACE.svc.cluster.local - --configdir config/opensearch-security/ - env: - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - volumeMounts: - - name: security-config - mountPath: /usr/share/opensearch/config/opensearch-security - readOnly: true - - name: tls-client - # The Java policy allows reading from /usr/share/opensearch/config. - mountPath: /usr/share/opensearch/config/tls-client - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - # Create a client TLS certificate to run securityadmin.sh - - name: tls-client - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: pod - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" - # serviceAccountName: opensearch-cluster-master - securityContext: - runAsUser: 1000 - runAsGroup: 0 - fsGroup: 1000 - restartPolicy: OnFailure From 9efc2cd9f9e0c365eb626f853f8d58f9cb176f99 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 3 Jul 2025 17:22:36 +0200 Subject: [PATCH 20/35] Improve code quality --- Cargo.lock | 35 ++- Cargo.nix | 80 ++++++- rust/operator-binary/src/controller.rs | 3 +- rust/operator-binary/src/controller/build.rs | 51 ++-- .../src/controller/node_config.rs | 224 ++++++++++++------ .../src/controller/validate.rs | 1 - rust/operator-binary/src/crd/mod.rs | 8 - rust/operator-binary/src/framework.rs | 4 +- .../src/framework/cluster_resources.rs | 2 +- .../src/framework/role_utils.rs | 1 - tests/templates/kuttl/smoke/10-assert.yaml | 162 ++++++++++++- .../kuttl/smoke/10-install-opensearch.yaml | 12 +- 12 files changed, 438 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc5c0d1..713d17e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -915,9 +915,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -925,7 +925,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -1290,14 +1290,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2127,9 +2138,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -2432,7 +2443,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -2585,7 +2596,7 @@ dependencies = [ "educe", "either", "futures 0.3.31", - "indexmap 2.9.0", + "indexmap 2.10.0", "json-patch", "k8s-openapi", "kube", @@ -2852,17 +2863,19 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.nix b/Cargo.nix index f7c90e5..391f69c 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -2818,9 +2818,9 @@ rec { }; "h2" = rec { crateName = "h2"; - version = "0.4.10"; + version = "0.4.11"; edition = "2021"; - sha256 = "19f0va87lhzrc0lmwkgcz1z0haf6glajb4icp0b7n50vdmkilhm9"; + sha256 = "118771sqbsa6cn48y9waxq24jx80f5xy8af0lq5ixq7ifsi51nhp"; authors = [ "Carl Lerche " "Sean McArthur " @@ -2854,7 +2854,7 @@ rec { } { name = "indexmap"; - packageId = "indexmap 2.9.0"; + packageId = "indexmap 2.10.0"; features = [ "std" ]; } { @@ -4123,11 +4123,11 @@ rec { "serde-1" = [ "serde" ]; }; }; - "indexmap 2.9.0" = rec { + "indexmap 2.10.0" = rec { crateName = "indexmap"; - version = "2.9.0"; + version = "2.10.0"; edition = "2021"; - sha256 = "07m15a571yywmvqyb7ms717q9n42b46badbpsmx215jrg7dhv9yf"; + sha256 = "0qd6g26gxzl6dbf132w48fa8rr95glly3jhbk90i29726d9xhk7y"; dependencies = [ { name = "equivalent"; @@ -4150,6 +4150,37 @@ rec { }; resolvedDefaultFeatures = [ "default" "std" ]; }; + "io-uring" = rec { + crateName = "io-uring"; + version = "0.7.8"; + edition = "2021"; + sha256 = "04whnj5a4pml44jhsmmf4p87bpgr7swkcijx4yjcng8900pj0vmq"; + libName = "io_uring"; + authors = [ + "quininer " + ]; + dependencies = [ + { + name = "bitflags"; + packageId = "bitflags"; + } + { + name = "cfg-if"; + packageId = "cfg-if"; + } + { + name = "libc"; + packageId = "libc"; + usesDefaultFeatures = false; + } + ]; + features = { + "bindgen" = [ "dep:bindgen" ]; + "direct-syscall" = [ "sc" ]; + "overwrite" = [ "bindgen" ]; + "sc" = [ "dep:sc" ]; + }; + }; "ipnet" = rec { crateName = "ipnet"; version = "2.11.0"; @@ -6951,9 +6982,9 @@ rec { }; "reqwest" = rec { crateName = "reqwest"; - version = "0.12.20"; + version = "0.12.22"; edition = "2021"; - sha256 = "04qqxghqszjxk4pl4vxa5qlwinkfx0vvjkk10vv2n3hkv6blrgza"; + sha256 = "0cbmfrcrk6wbg93apmji0fln1ca9322af2kc7dpa18vcgs9k3jfb"; authors = [ "Sean McArthur " ]; @@ -7160,7 +7191,7 @@ rec { "h2" = [ "dep:h2" ]; "hickory-dns" = [ "dep:hickory-resolver" "dep:once_cell" ]; "http2" = [ "h2" "hyper/http2" "hyper-util/http2" "hyper-rustls?/http2" ]; - "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "dep:slab" "tokio/macros" ]; + "http3" = [ "rustls-tls-manual-roots" "dep:h3" "dep:h3-quinn" "dep:quinn" "tokio/macros" ]; "json" = [ "dep:serde_json" ]; "macos-system-configuration" = [ "system-proxy" ]; "multipart" = [ "dep:mime_guess" "dep:futures-util" ]; @@ -8001,7 +8032,7 @@ rec { dependencies = [ { name = "indexmap"; - packageId = "indexmap 2.9.0"; + packageId = "indexmap 2.10.0"; } { name = "itoa"; @@ -8481,7 +8512,7 @@ rec { } { name = "indexmap"; - packageId = "indexmap 2.9.0"; + packageId = "indexmap 2.10.0"; } { name = "json-patch"; @@ -9344,9 +9375,9 @@ rec { }; "tokio" = rec { crateName = "tokio"; - version = "1.45.1"; + version = "1.46.0"; edition = "2021"; - sha256 = "0yb7h0mr0m0gfwdl1jir2k37gcrwhcib2kiyx9f95npi7sim3vvm"; + sha256 = "1i1ypwjxrsxz1w14qyvj3smcandl6dsg6h85w75shmhp920bnh0i"; authors = [ "Tokio Contributors " ]; @@ -9361,6 +9392,17 @@ rec { packageId = "bytes"; optional = true; } + { + name = "io-uring"; + packageId = "io-uring"; + usesDefaultFeatures = false; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + } + { + name = "libc"; + packageId = "libc"; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + } { name = "libc"; packageId = "libc"; @@ -9373,6 +9415,13 @@ rec { optional = true; usesDefaultFeatures = false; } + { + name = "mio"; + packageId = "mio"; + usesDefaultFeatures = false; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + features = [ "os-poll" "os-ext" ]; + } { name = "parking_lot"; packageId = "parking_lot"; @@ -9388,6 +9437,11 @@ rec { optional = true; target = { target, features }: (target."unix" or false); } + { + name = "slab"; + packageId = "slab"; + target = { target, features }: ((target."tokio_uring" or false) && ("linux" == target."os" or null)); + } { name = "socket2"; packageId = "socket2"; diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 97cc58b..050af33 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -101,8 +101,7 @@ type OpenSearchRoleGroupConfig = // validated and converted to validated and safe types // no user errors // not restricted by CRD compliance -// TODO More derives -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct ValidatedCluster { metadata: ObjectMeta, pub image: ProductImage, diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 20f26b9..f147ece 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -30,7 +30,7 @@ use super::{ use crate::{ crd::v1alpha1, framework::{ - RoleName, + ClusterName, OBJECT_NAME_MAX_LENGTH, RoleName, builder::{ meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, }, @@ -78,13 +78,13 @@ impl<'a> RoleBuilder<'a> { self.cluster.clone(), role_group_name.clone(), role_group_config.clone(), + self.discovery_service_name(), ) }) .collect() } fn build_cluster_manager_service(&self) -> Service { - // TODO Share port config let ports = vec![ ServicePort { name: Some(HTTP_PORT_NAME.to_owned()), @@ -127,7 +127,7 @@ impl<'a> RoleBuilder<'a> { .unwrap(); let metadata = ObjectMetaBuilder::new() - .name(format!("{}-cluster-manager", self.cluster.name)) + .name(self.discovery_service_name()) .namespace(&self.cluster.namespace) .ownerreference(ownerreference_from_resource( &self.cluster, @@ -182,8 +182,23 @@ impl<'a> RoleBuilder<'a> { None } } + + fn discovery_service_name(&self) -> String { + const SUFFIX: &str = "-cluster-manager"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, + "The resource name `-cluster-manager` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.cluster.name) + } } +// Path in opensearchproject/opensearch:3.0.0 +const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; + struct RoleGroupBuilder<'a> { names: &'a ContextNames, role_name: RoleName, @@ -201,6 +216,7 @@ impl<'a> RoleGroupBuilder<'a> { cluster: ValidatedCluster, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, + discovery_service_name: String, ) -> RoleGroupBuilder<'a> { // used for the name of the StatefulSet, role-group ConfigMap, ... let qualified_role_group_name = @@ -210,7 +226,12 @@ impl<'a> RoleGroupBuilder<'a> { names, role_name: role_name.clone(), cluster: cluster.clone(), - node_config: NodeConfig::new(role_name, cluster), + node_config: NodeConfig::new( + role_name, + cluster, + role_group_config.clone(), + discovery_service_name, + ), qualified_role_group_name, role_group_name, role_group_config, @@ -231,8 +252,7 @@ impl<'a> RoleGroupBuilder<'a> { let data = [( CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), - self.node_config - .static_opensearch_config(&self.role_group_config), + self.node_config.static_opensearch_config(), )] .into(); @@ -351,18 +371,15 @@ impl<'a> RoleGroupBuilder<'a> { ContainerBuilder::new("opensearch") .expect("should be a valid container name") .image_from_product_image(&product_image) - .command(vec![ - "/usr/share/opensearch/opensearch-docker-entrypoint.sh".to_owned(), - ]) + .command(vec![format!( + "{OPENSEARCH_BASE_PATH}/opensearch-docker-entrypoint.sh" + )]) .args(role_group_config.cli_overrides_to_vec()) - .add_env_vars( - self.node_config - .environment_variables(role_group_config) - .into(), - ) + .add_env_vars(self.node_config.environment_variables().into()) .add_volume_mounts([VolumeMount { - // TODO Use path and file constants - mount_path: "/usr/share/opensearch/config/opensearch.yml".to_owned(), + mount_path: format!( + "{OPENSEARCH_BASE_PATH}/config/{CONFIGURATION_FILE_OPENSEARCH_YML}" + ), name: CONFIG_VOLUME_NAME.to_owned(), read_only: Some(true), sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), @@ -422,7 +439,7 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; - // TODO Add metrics port and Prometheus label + // TODO Add Prometheus label let metadata = ObjectMetaBuilder::new() .name(self.qualified_role_group_name.clone()) diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/node_config.rs index 1d725c5..034e407 100644 --- a/rust/operator-binary/src/controller/node_config.rs +++ b/rust/operator-binary/src/controller/node_config.rs @@ -1,98 +1,169 @@ -use std::collections::BTreeMap; - +use serde_json::{Value, json}; use stackable_operator::builder::pod::container::FieldPathEnvVar; use super::{OpenSearchRoleGroupConfig, ValidatedCluster}; use crate::{ - crd::{NodeRoles, v1alpha1}, + crd::v1alpha1, framework::{RoleName, builder::pod::container::EnvVarSet, to_qualified_role_group_name}, }; pub const CONFIGURATION_FILE_OPENSEARCH_YML: &str = "opensearch.yml"; + +// TODO Document how to enter config_overrides of various types, e.g. string, list, boolean, ... + +// Configuration file format +// +// This is not well documented. +// +// A list setting can be written as +// - a comma-separated list, e.g. +// ``` +// setting: a,b,c +// ``` +// Commas in the values cannot be escaped. +// - a JSON list, e.g. +// ``` +// setting: ["a", "b", "c"] +// ``` +// - a YAML list, e.g. +// ``` +// setting: +// - a +// - b +// - c +// ``` +// - a (legacy) flat list, e.g. +// ``` +// setting.0: a +// setting.1: b +// setting.2: b +// ``` + +/// type: string pub const CONFIG_OPTION_CLUSTER_NAME: &str = "cluster.name"; -pub const DISCOVERY_SEED_HOSTS: &str = "discovery.seed_hosts"; -pub const DISCOVERY_TYPE: &str = "discovery.type"; -pub const INITIAL_CLUSTER_MANAGER_NODES: &str = "cluster.initial_cluster_manager_nodes"; -pub const NETWORK_HOST: &str = "network.host"; -pub const NODE_NAME: &str = "node.name"; -pub const NODE_ROLES: &str = "node.roles"; + +/// type: list of strings +pub const CONFIG_OPTION_DISCOVERY_SEED_HOSTS: &str = "discovery.seed_hosts"; + +/// type: string +pub const CONFIG_OPTION_DISCOVERY_TYPE: &str = "discovery.type"; + +/// type: list of strings +pub const CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES: &str = + "cluster.initial_cluster_manager_nodes"; + +/// type: string +pub const CONFIG_OPTION_NETWORK_HOST: &str = "network.host"; + +/// type: string +pub const CONFIG_OPTION_NODE_NAME: &str = "node.name"; + +/// type: list of strings +pub const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; + +/// type: list of strings +pub const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; pub struct NodeConfig { role_name: RoleName, cluster: ValidatedCluster, + role_group_config: OpenSearchRoleGroupConfig, + discovery_service_name: String, } // Most functions are public because their configuration values could also be used in environment // variables. impl NodeConfig { - pub fn new(role_name: RoleName, cluster: ValidatedCluster) -> Self { - Self { role_name, cluster } + pub fn new( + role_name: RoleName, + cluster: ValidatedCluster, + role_group_config: OpenSearchRoleGroupConfig, + discovery_service_name: String, + ) -> Self { + Self { + role_name, + cluster, + role_group_config, + discovery_service_name, + } } /// static for the cluster - pub fn static_opensearch_config( - &self, - // TODO only config overrides - role_group_config: &OpenSearchRoleGroupConfig, - ) -> String { - let mut config: BTreeMap = [ - (CONFIG_OPTION_CLUSTER_NAME.to_owned(), self.cluster_name()), - (NETWORK_HOST.to_owned(), self.network_host()), - (DISCOVERY_TYPE.to_owned(), self.discovery_type()), - ] - .into(); - - config.extend( - role_group_config - .config_overrides - .get(CONFIGURATION_FILE_OPENSEARCH_YML) - .cloned() - .unwrap_or_default(), + pub fn static_opensearch_config(&self) -> String { + let mut config = serde_json::Map::new(); + + config.insert( + CONFIG_OPTION_CLUSTER_NAME.to_owned(), + json!(self.cluster.name.to_string()), ); + config.insert( + CONFIG_OPTION_NETWORK_HOST.to_owned(), + // Bind to all interfaces because the IP address is not known in advance. + json!("0.0.0.0".to_owned()), + ); + config.insert( + CONFIG_OPTION_DISCOVERY_TYPE.to_owned(), + json!(self.discovery_type()), + ); + config.insert + // Accept certificates generated by the secret-operator + ( + CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN.to_owned(), + json!(["CN=generated certificate for pod".to_owned()]), + ); + + for (setting, value) in self + .role_group_config + .config_overrides + .get(CONFIGURATION_FILE_OPENSEARCH_YML) + .into_iter() + .flatten() + { + config.insert(setting.to_owned(), json!(value)); + } + + // Ensure a deterministic result + config.sort_keys(); - NodeConfig::to_yaml(config) + Self::to_yaml(config) } /// different for every node - pub fn environment_variables( - &self, - // only node roles? - role_group_config: &OpenSearchRoleGroupConfig, - ) -> EnvVarSet { + pub fn environment_variables(&self) -> EnvVarSet { EnvVarSet::new() // Set the OpenSearch node name to the Pod name. // The node name is used e.g. for `{INITIAL_CLUSTER_MANAGER_NODES}`. - .with_field_path(NODE_NAME, FieldPathEnvVar::Name) - // TODO DISCOVERY_SEED_HOSTS to opensearch.yml? - .with_value(DISCOVERY_SEED_HOSTS, self.discovery_seed_hosts()) + .with_field_path(CONFIG_OPTION_NODE_NAME, FieldPathEnvVar::Name) .with_value( - INITIAL_CLUSTER_MANAGER_NODES, - self.initial_cluster_manager_nodes(&role_group_config.config.node_roles), + CONFIG_OPTION_DISCOVERY_SEED_HOSTS, + &self.discovery_service_name, ) .with_value( - NODE_ROLES, - self.node_roles(&role_group_config.config.node_roles), + CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES, + self.initial_cluster_manager_nodes(), ) - .with_values(role_group_config.env_overrides.clone()) + .with_value( + CONFIG_OPTION_NODE_ROLES, + self.role_group_config + .config + .node_roles + .iter() + .map(|node_role| format!("{node_role}")) + .collect::>() + // Node roles cannot contain commas, therefore creating a comma-separated list + // is safe. + .join(","), + ) + .with_values(self.role_group_config.env_overrides.clone()) } - fn to_yaml(kv: BTreeMap) -> String { - // TODO Do it right! + fn to_yaml(kv: serde_json::Map) -> String { kv.iter() .map(|(key, value)| format!("{key}: {value}")) .collect::>() .join("\n") } - pub fn cluster_name(&self) -> String { - self.cluster.name.to_string() - } - - pub fn discovery_seed_hosts(&self) -> String { - // TODO Check length - format!("{}-cluster-manager", self.cluster_name()) - } - /// Configuration for `{DISCOVERY_TYPE}` /// /// "zen" is the default if `{DISCOVERY_TYPE}` is not set. @@ -128,9 +199,13 @@ impl NodeConfig { /// The OpenSearch Helm chart only sets master nodes but does not handle the other cases (see /// https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/templates/statefulset.yaml#L414-L415), /// so they are also ignored here for the moment. - pub fn initial_cluster_manager_nodes(&self, node_roles: &NodeRoles) -> String { + fn initial_cluster_manager_nodes(&self) -> String { if !self.cluster.is_single_node() - && node_roles.contains(&v1alpha1::NodeRole::ClusterManager) + && self + .role_group_config + .config + .node_roles + .contains(&v1alpha1::NodeRole::ClusterManager) { let cluster_manager_configs = self .cluster @@ -150,6 +225,7 @@ impl NodeConfig { pod_names .extend((0..role_group_config.replicas).map(|i| format!("{sts_name}-{i}"))); } + // Pod names cannot contain commas, therefore creating a comma-separated list is safe. pod_names.join(",") } else { // This setting is not allowed on single node cluster, see @@ -157,25 +233,15 @@ impl NodeConfig { String::new() } } - - pub fn network_host(&self) -> String { - // Bind to all interfaces because the IP address is not known in advance. - "0.0.0.0".to_owned() - } - - pub fn node_roles(&self, node_roles: &NodeRoles) -> String { - node_roles - .iter() - .map(|node_role| format!("{node_role}")) - .collect::>() - .join(",") - } } #[cfg(test)] mod tests { - use std::{collections::HashMap, str::FromStr}; + use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, + }; use stackable_operator::{ commons::product_image_selection::ProductImage, @@ -185,8 +251,9 @@ mod tests { }; use super::*; - use crate::framework::{ - ClusterName, ProductVersion, role_utils::GenericProductSpecificCommonConfig, + use crate::{ + crd::NodeRoles, + framework::{ClusterName, ProductVersion, role_utils::GenericProductSpecificCommonConfig}, }; #[test] @@ -207,8 +274,6 @@ mod tests { }; let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); - let node_config = NodeConfig::new(role_name, cluster); - let role_group_config = OpenSearchRoleGroupConfig { replicas: 1, config: v1alpha1::OpenSearchConfig { @@ -221,7 +286,14 @@ mod tests { product_specific_common_config: GenericProductSpecificCommonConfig::default(), }; - let env_vars = node_config.environment_variables(&role_group_config); + let node_config = NodeConfig::new( + role_name, + cluster, + role_group_config, + "my-opensearch-cluster-manager".to_owned(), + ); + + let env_vars = node_config.environment_variables(); // TODO Test EnvVarSet and compare EnvVarSets assert_eq!( @@ -238,7 +310,7 @@ mod tests { }, EnvVar { name: "discovery.seed_hosts".to_owned(), - value: Some("my-opensearch-cluster-cluster-manager".to_owned()), + value: Some("my-opensearch-cluster-manager".to_owned()), value_from: None }, EnvVar { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 6d154de..46a6182 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -16,7 +16,6 @@ use crate::{ #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { - // TODO Improve message #[snafu(display("failed to get the cluster name"))] GetClusterName {}, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index b20d5c4..4c1afea 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -139,14 +139,6 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct NodeRoles(Vec); -// impl Iterator for NodeRoles { -// type Item = v1alpha1::NodeRole; -// -// fn next(&mut self) -> Option { -// self. -// } -// } - impl NodeRoles { pub fn contains(&self, node_role: &v1alpha1::NodeRole) -> bool { self.0.contains(node_role) diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index ed27a8b..552d631 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -32,7 +32,7 @@ pub enum Error { /// Maximum length of a DNS subdomain name as defined in RFC 1123. #[allow(dead_code)] -const OBJECT_NAME_MAX_LENGTH: usize = 253; +pub const OBJECT_NAME_MAX_LENGTH: usize = 253; /// Has a name that can be used as a DNS subdomain name as defined in RFC 1123. /// Most resource types, e.g. a Pod, require such a compliant name. @@ -136,6 +136,8 @@ attributed_string_type! { attributed_string_type! { ClusterName, "The name of a cluster/stacklet, e.g. \"my-opensearch-cluster\"", + // Suffixes are added to produce a resource names. According compile-time check ensures that + // max_length cannot be set higher. (max_length = LABEL_VALUE_MAX_LENGTH), is_object_name, is_valid_label_value diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs index ffdfd12..40dea63 100644 --- a/rust/operator-binary/src/framework/cluster_resources.rs +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -19,7 +19,7 @@ pub fn cluster_resources_new( // `-operator`. For the resulting label value to be valid, it must not exceed 63 characters. // Check at compile time that ProductName::MAX_LENGTH is defined accordingly. const _: () = assert!( - ProductName::MAX_LENGTH <= LABEL_VALUE_MAX_LENGTH - "-operator".len(), + ProductName::MAX_LENGTH + "-operator".len() <= LABEL_VALUE_MAX_LENGTH, "The label value `-operator` must not exceed 63 characters." ); diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs index e840589..cba93ef 100644 --- a/rust/operator-binary/src/framework/role_utils.rs +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -110,7 +110,6 @@ where role.config.pod_overrides.clone(), role_group.config.pod_overrides.clone(), ), - // TODO Merge product_specific_common_config: merged_product_specific_common_config( role.config.product_specific_common_config.clone(), role_group.config.product_specific_common_config.clone(), diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index 812cddb..f6faa3e 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -51,8 +51,8 @@ spec: env: - name: DISABLE_INSTALL_DEMO_CONFIG value: "true" - - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD - value: super@Secret1 + # - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD + # value: super@Secret1 - name: cluster.initial_cluster_manager_nodes value: opensearch-nodes-cluster-manager-0,opensearch-nodes-cluster-manager-1,opensearch-nodes-cluster-manager-2 - name: discovery.seed_hosts @@ -180,8 +180,8 @@ spec: env: - name: DISABLE_INSTALL_DEMO_CONFIG value: "true" - - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD - value: super@Secret1 + # - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD + # value: super@Secret1 - name: cluster.initial_cluster_manager_nodes - name: discovery.seed_hosts value: opensearch-cluster-manager @@ -260,6 +260,160 @@ status: readyReplicas: 5 replicas: 5 --- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-cluster-manager +data: + opensearch.yml: |- + cluster.name: "opensearch" + discovery.type: "zen" + network.host: "0.0.0.0" + plugins.security.allow_default_init_securityindex: "true" + plugins.security.nodes_dn: ["CN=generated certificate for pod"] + plugins.security.ssl.http.enabled: "true" + plugins.security.ssl.http.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" + plugins.security.ssl.transport.enabled: "true" + plugins.security.ssl.transport.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-data +data: + opensearch.yml: |- + cluster.name: "opensearch" + discovery.type: "zen" + network.host: "0.0.0.0" + plugins.security.allow_default_init_securityindex: "true" + plugins.security.nodes_dn: ["CN=generated certificate for pod"] + plugins.security.ssl.http.enabled: "true" + plugins.security.ssl.http.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" + plugins.security.ssl.transport.enabled: "true" + plugins.security.ssl.transport.pemcert_filepath: "/usr/share/opensearch/config/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "/usr/share/opensearch/config/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "/usr/share/opensearch/config/tls/ca.crt" +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-cluster-manager +spec: + ports: + - name: http + port: 9200 + protocol: TCP + targetPort: 9200 + - name: transport + port: 9300 + protocol: TCP + targetPort: 9300 + - name: metrics + port: 9600 + protocol: TCP + targetPort: 9600 + publishNotReadyAddresses: true + selector: + cluster_manager: "true" + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-cluster-manager +spec: + ports: + - name: http + port: 9200 + protocol: TCP + targetPort: 9200 + - name: transport + port: 9300 + protocol: TCP + targetPort: 9300 + - name: metrics + port: 9600 + protocol: TCP + targetPort: 9600 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-nodes-data +spec: + ports: + - name: http + port: 9200 + protocol: TCP + targetPort: 9200 + - name: transport + port: 9300 + protocol: TCP + targetPort: 9300 + - name: metrics + port: 9600 + protocol: TCP + targetPort: 9600 + publishNotReadyAddresses: true + selector: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: data + type: ClusterIP +--- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 7df5a40..d24e83f 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -42,20 +42,12 @@ spec: envOverrides: # TODO Make these the defaults in the image DISABLE_INSTALL_DEMO_CONFIG: "true" - OPENSEARCH_INITIAL_ADMIN_PASSWORD: super@Secret1 + # OPENSEARCH_INITIAL_ADMIN_PASSWORD: super@Secret1 configOverrides: # TODO Add the required options to the operator opensearch.yml: plugins.security.allow_default_init_securityindex: "true" - # Accept the client TLS certificate generated by the - # secret-operator - # The CN matches the one of the nodes_dn and therefore - # securityadmin.sh throws an error that this is forbidden, - # but it seems to work nevertheless. - plugins.security.authcz.admin_dn.0: CN=generated certificate for pod - # Accept certificates generated by the secret-operator - plugins.security.nodes_dn: "[\"CN=generated certificate for pod\"]" - plugins.security.restapi.roles_enabled: "[\"all_access\"]" + plugins.security.ssl.transport.enabled: "true" plugins.security.ssl.transport.pemcert_filepath: /usr/share/opensearch/config/tls/tls.crt plugins.security.ssl.transport.pemkey_filepath: /usr/share/opensearch/config/tls/tls.key plugins.security.ssl.transport.pemtrustedcas_filepath: /usr/share/opensearch/config/tls/ca.crt From 9c10ee931d802f399dea8f08f59c268b4e12f6d1 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 8 Jul 2025 16:26:43 +0200 Subject: [PATCH 21/35] Add resource configuration and defaults --- rust/operator-binary/src/controller.rs | 3 + rust/operator-binary/src/controller/build.rs | 41 +++++++--- .../src/controller/node_config.rs | 8 +- .../src/controller/validate.rs | 4 +- rust/operator-binary/src/crd/mod.rs | 82 ++++++++++++++++--- .../src/framework/cluster_resources.rs | 2 +- tests/templates/kuttl/smoke/10-assert.yaml | 57 ++++++++++++- .../kuttl/smoke/10-install-opensearch.yaml | 12 ++- 8 files changed, 177 insertions(+), 32 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 050af33..b01868d 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -240,6 +240,9 @@ pub async fn reconcile( .await .context(ApplyResourcesSnafu)?; + // create discovery ConfigMap + // TODO Think about: Address from Listener has to be added to some ConfigMap + // update status (client required) update_status(&context.client, &context.names, cluster, applied_resources) .await diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index f147ece..3ddd5d8 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -41,6 +41,7 @@ use crate::{ const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; const CONFIG_VOLUME_NAME: &str = "config"; +const DATA_VOLUME_NAME: &str = "data"; const HTTP_PORT_NAME: &str = "http"; const HTTP_PORT: u16 = 9200; const TRANSPORT_PORT_NAME: &str = "transport"; @@ -275,8 +276,6 @@ impl<'a> RoleGroupBuilder<'a> { .with_labels(self.build_recommended_labels()) .build(); - let template = self.build_pod_template(); - let statefulset_match_labels = role_group_selector( &self.cluster, &self.names.product_name, @@ -284,6 +283,17 @@ impl<'a> RoleGroupBuilder<'a> { &self.role_group_name, ); + let template = self.build_pod_template(); + + let data_volume_claim_template = self + .role_group_config + .config + .resources + .storage + .data + // TODO Compare name with Helm chart + .build_pvc(DATA_VOLUME_NAME, Some(vec!["ReadWriteOnce"])); + let spec = StatefulSetSpec { // Order does not matter for OpenSearch pod_management_policy: Some("Parallel".to_string()), @@ -294,6 +304,7 @@ impl<'a> RoleGroupBuilder<'a> { }, service_name: None, template, + volume_claim_templates: Some(vec![data_volume_claim_template]), ..StatefulSetSpec::default() }; @@ -376,15 +387,22 @@ impl<'a> RoleGroupBuilder<'a> { )]) .args(role_group_config.cli_overrides_to_vec()) .add_env_vars(self.node_config.environment_variables().into()) - .add_volume_mounts([VolumeMount { - mount_path: format!( - "{OPENSEARCH_BASE_PATH}/config/{CONFIGURATION_FILE_OPENSEARCH_YML}" - ), - name: CONFIG_VOLUME_NAME.to_owned(), - read_only: Some(true), - sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), - ..VolumeMount::default() - }]) + .add_volume_mounts([ + VolumeMount { + mount_path: format!( + "{OPENSEARCH_BASE_PATH}/config/{CONFIGURATION_FILE_OPENSEARCH_YML}" + ), + name: CONFIG_VOLUME_NAME.to_owned(), + read_only: Some(true), + sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{OPENSEARCH_BASE_PATH}/data"), + name: DATA_VOLUME_NAME.to_owned(), + ..VolumeMount::default() + }, + ]) .expect("The mount paths are statically defined and there should be no duplicates.") .add_container_ports(vec![ ContainerPort { @@ -403,6 +421,7 @@ impl<'a> RoleGroupBuilder<'a> { ..ContainerPort::default() }, ]) + .resources(self.role_group_config.config.resources.clone().into()) .startup_probe(startup_probe) .readiness_probe(readiness_probe) .build() diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/node_config.rs index 034e407..ad75951 100644 --- a/rust/operator-binary/src/controller/node_config.rs +++ b/rust/operator-binary/src/controller/node_config.rs @@ -1,3 +1,5 @@ +// TODO Create build module and move build.rs and this file into it + use serde_json::{Value, json}; use stackable_operator::builder::pod::container::FieldPathEnvVar; @@ -9,7 +11,8 @@ use crate::{ pub const CONFIGURATION_FILE_OPENSEARCH_YML: &str = "opensearch.yml"; -// TODO Document how to enter config_overrides of various types, e.g. string, list, boolean, ... +// TODO Document how to enter config_overrides of various types, e.g. string, list, boolean, +// object, ... // Configuration file format // @@ -244,7 +247,7 @@ mod tests { }; use stackable_operator::{ - commons::product_image_selection::ProductImage, + commons::{product_image_selection::ProductImage, resources::Resources}, k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector, PodTemplateSpec}, kube::api::ObjectMeta, role_utils::GenericRoleConfig, @@ -278,6 +281,7 @@ mod tests { replicas: 1, config: v1alpha1::OpenSearchConfig { node_roles: NodeRoles::default(), + resources: Resources::default(), }, config_overrides: HashMap::default(), env_overrides: [("TEST".to_owned(), "value".to_owned())].into(), diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 46a6182..0cb83f3 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -62,13 +62,11 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result, - // ingest: Option<()>, - // cluster_manager: Option<()>, - // remote_cluster_client: Option<()>, - // warm: Option<()>, - // search: Option<()>, - // } - #[derive(Clone, Debug, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( derive( @@ -114,6 +112,28 @@ pub mod versioned { )] pub struct OpenSearchConfig { pub node_roles: NodeRoles, + + #[fragment_attrs(serde(default))] + pub resources: Resources, + } + + #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] + #[fragment_attrs( + derive( + Clone, + Debug, + Default, + Deserialize, + Merge, + JsonSchema, + PartialEq, + Serialize + ), + serde(rename_all = "camelCase") + )] + pub struct StorageConfig { + #[fragment_attrs(serde(default))] + pub data: PvcConfig, } #[derive(Clone, Default, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -136,6 +156,46 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { } } +impl v1alpha1::OpenSearchConfig { + pub fn default_config() -> v1alpha1::OpenSearchConfigFragment { + v1alpha1::OpenSearchConfigFragment { + resources: ResourcesFragment { + memory: MemoryLimitsFragment { + // An idle node already requires 2 Gi. + limit: Some(Quantity("2Gi".to_owned())), + runtime_limits: NoRuntimeLimitsFragment {}, + }, + cpu: CpuLimitsFragment { + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L150 + min: Some(Quantity("1".to_owned())), + // an arbitrary value + max: Some(Quantity("4".to_owned())), + }, + storage: v1alpha1::StorageConfigFragment { + data: PvcConfigFragment { + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L220 + // This value should be overriden by the user. Data nodes need probably + // more, the other nodes less. + capacity: Some(Quantity("8Gi".to_owned())), + storage_class: None, + selectors: None, + }, + }, + }, + // Defaults taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L16-L20 + node_roles: Some(NodeRoles(vec![ + v1alpha1::NodeRole::ClusterManager, + v1alpha1::NodeRole::Ingest, + v1alpha1::NodeRole::Data, + v1alpha1::NodeRole::RemoteClusterClient, + ])), + } + } +} + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct NodeRoles(Vec); diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs index 40dea63..de27104 100644 --- a/rust/operator-binary/src/framework/cluster_resources.rs +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -35,5 +35,5 @@ pub fn cluster_resources_new( }, apply_strategy, ) - .expect("") + .expect("ClusterResources should be created because the cluster object reference contains name, namespace and uid.") } diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index f6faa3e..2297ba1 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -84,6 +84,13 @@ spec: tcpSocket: port: http timeoutSeconds: 3 + resources: + limits: + cpu: "4" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi startupProbe: failureThreshold: 30 initialDelaySeconds: 5 @@ -97,6 +104,8 @@ spec: name: config readOnly: true subPath: opensearch.yml + - mountPath: /usr/share/opensearch/data + name: data - mountPath: /usr/share/opensearch/config/opensearch-security name: security-config readOnly: true @@ -105,8 +114,10 @@ spec: readOnly: true securityContext: fsGroup: 1000 + terminationGracePeriodSeconds: 30 volumes: - configMap: + defaultMode: 420 name: opensearch-nodes-cluster-manager name: config - ephemeral: @@ -129,6 +140,20 @@ spec: secret: defaultMode: 420 secretName: opensearch-security-config + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi + volumeMode: Filesystem + status: + phase: Pending status: readyReplicas: 3 replicas: 3 @@ -152,7 +177,7 @@ metadata: name: opensearch spec: podManagementPolicy: Parallel - replicas: 5 + replicas: 2 selector: matchLabels: app.kubernetes.io/component: nodes @@ -212,6 +237,13 @@ spec: tcpSocket: port: http timeoutSeconds: 3 + resources: + limits: + cpu: "4" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi startupProbe: failureThreshold: 30 initialDelaySeconds: 5 @@ -225,6 +257,8 @@ spec: name: config readOnly: true subPath: opensearch.yml + - mountPath: /usr/share/opensearch/data + name: data - mountPath: /usr/share/opensearch/config/opensearch-security name: security-config readOnly: true @@ -233,8 +267,10 @@ spec: readOnly: true securityContext: fsGroup: 1000 + terminationGracePeriodSeconds: 30 volumes: - configMap: + defaultMode: 420 name: opensearch-nodes-data name: config - ephemeral: @@ -255,10 +291,25 @@ spec: name: tls - name: security-config secret: + defaultMode: 420 secretName: opensearch-security-config + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + volumeMode: Filesystem + status: + phase: Pending status: - readyReplicas: 5 - replicas: 5 + readyReplicas: 2 + replicas: 2 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index d24e83f..8b80df7 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -1,3 +1,4 @@ +# TODO Test with OpenShift --- apiVersion: opensearch.stackable.tech/v1alpha1 kind: OpenSearchCluster @@ -13,6 +14,10 @@ spec: config: nodeRoles: - cluster_manager + resources: + storage: + data: + capacity: 100Mi replicas: 3 podOverrides: spec: @@ -29,7 +34,11 @@ spec: - ingest - data - remote_cluster_client - replicas: 5 + resources: + storage: + data: + capacity: 2Gi + replicas: 2 podOverrides: spec: volumes: @@ -46,6 +55,7 @@ spec: configOverrides: # TODO Add the required options to the operator opensearch.yml: + # TODO Check that this is safe despite the warning in the documentation plugins.security.allow_default_init_securityindex: "true" plugins.security.ssl.transport.enabled: "true" plugins.security.ssl.transport.pemcert_filepath: /usr/share/opensearch/config/tls/tls.crt From 1dcae8b831621f29ec66180ed3af9a9c13f72e8d Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 8 Jul 2025 16:57:38 +0200 Subject: [PATCH 22/35] Restructure build module --- rust/operator-binary/src/controller.rs | 1 - rust/operator-binary/src/controller/build.rs | 499 +----------------- .../src/controller/{ => build}/node_config.rs | 5 +- .../src/controller/build/role_builder.rs | 176 ++++++ .../controller/build/role_group_builder.rs | 338 ++++++++++++ 5 files changed, 522 insertions(+), 497 deletions(-) rename rust/operator-binary/src/controller/{ => build}/node_config.rs (98%) create mode 100644 rust/operator-binary/src/controller/build/role_builder.rs create mode 100644 rust/operator-binary/src/controller/build/role_group_builder.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index b01868d..16c4c26 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -31,7 +31,6 @@ use crate::{ mod apply; mod build; -mod node_config; mod update_status; mod validate; diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 3ddd5d8..fb9d6c9 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -1,500 +1,13 @@ use std::{marker::PhantomData, str::FromStr}; -use stackable_operator::{ - builder::{ - meta::ObjectMetaBuilder, - pod::{PodBuilder, container::ContainerBuilder}, - }, - k8s_openapi::{ - DeepMerge, - api::{ - apps::v1::{StatefulSet, StatefulSetSpec}, - core::v1::{ - ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodTemplateSpec, Probe, - Service, ServicePort, ServiceSpec, TCPSocketAction, Volume, VolumeMount, - }, - policy::v1::PodDisruptionBudget, - }, - apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, - }, - kvp::{ - Label, Labels, - consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, - }, -}; +use role_builder::RoleBuilder; -use super::{ - ContextNames, OpenSearchRoleGroupConfig, Prepared, Resources, RoleGroupName, ValidatedCluster, - node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}, -}; -use crate::{ - crd::v1alpha1, - framework::{ - ClusterName, OBJECT_NAME_MAX_LENGTH, RoleName, - builder::{ - meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, - }, - kvp::label::{recommended_labels, role_group_selector}, - to_qualified_role_group_name, - }, -}; +use super::{ContextNames, Prepared, Resources, ValidatedCluster}; +use crate::framework::RoleName; -const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; -const CONFIG_VOLUME_NAME: &str = "config"; -const DATA_VOLUME_NAME: &str = "data"; -const HTTP_PORT_NAME: &str = "http"; -const HTTP_PORT: u16 = 9200; -const TRANSPORT_PORT_NAME: &str = "transport"; -const TRANSPORT_PORT: u16 = 9300; -const METRICS_PORT_NAME: &str = "metrics"; -const METRICS_PORT: u16 = 9600; - -struct RoleBuilder<'a> { - names: &'a ContextNames, - role_name: RoleName, - cluster: ValidatedCluster, -} - -impl<'a> RoleBuilder<'a> { - fn new( - names: &'a ContextNames, - role_name: RoleName, - cluster: ValidatedCluster, - ) -> RoleBuilder<'a> { - RoleBuilder { - names, - role_name: role_name.clone(), - cluster: cluster.clone(), - } - } - - fn role_group_builders(&self) -> Vec { - self.cluster - .role_group_configs - .iter() - .map(|(role_group_name, role_group_config)| { - RoleGroupBuilder::new( - self.names, - self.role_name.clone(), - self.cluster.clone(), - role_group_name.clone(), - role_group_config.clone(), - self.discovery_service_name(), - ) - }) - .collect() - } - - fn build_cluster_manager_service(&self) -> Service { - let ports = vec![ - ServicePort { - name: Some(HTTP_PORT_NAME.to_owned()), - port: HTTP_PORT.into(), - ..ServicePort::default() - }, - ServicePort { - name: Some(TRANSPORT_PORT_NAME.to_owned()), - port: TRANSPORT_PORT.into(), - ..ServicePort::default() - }, - ServicePort { - name: Some(METRICS_PORT_NAME.to_owned()), - port: METRICS_PORT.into(), - ..ServicePort::default() - }, - ]; - - // Well-known Kubernetes labels - let mut labels = Labels::role_selector( - &self.cluster, - &self.names.product_name.to_string(), - &self.role_name.to_string(), - ) - .unwrap(); - - let managed_by = Label::managed_by( - &self.names.operator_name.to_string(), - &self.names.controller_name.to_string(), - ) - .unwrap(); - let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); - - labels.insert(managed_by); - labels.insert(version); - - // Stackable-specific labels - labels - .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) - .unwrap(); - - let metadata = ObjectMetaBuilder::new() - .name(self.discovery_service_name()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(labels) - .build(); - - let service_selector = [( - v1alpha1::NodeRole::ClusterManager.to_string(), - "true".to_owned(), - )] - .into(); - - let service_spec = ServiceSpec { - // Internal communication does not need to be exposed - type_: Some("ClusterIP".to_string()), - cluster_ip: Some("None".to_string()), - ports: Some(ports), - selector: Some(service_selector), - publish_not_ready_addresses: Some(true), - ..ServiceSpec::default() - }; - - Service { - metadata, - spec: Some(service_spec), - status: None, - } - } - - fn build_pdb(&self) -> Option { - let pdb_config = &self.cluster.role_config.pod_disruption_budget; - - if pdb_config.enabled { - let max_unavailable = pdb_config - .max_unavailable - .unwrap_or(PDB_DEFAULT_MAX_UNAVAILABLE); - Some( - pod_disruption_budget_builder_with_role( - &self.cluster, - &self.names.product_name, - &self.role_name, - &self.names.operator_name, - &self.names.controller_name, - ) - .with_max_unavailable(max_unavailable) - .build(), - ) - } else { - None - } - } - - fn discovery_service_name(&self) -> String { - const SUFFIX: &str = "-cluster-manager"; - - // Compile-time check - const _: () = assert!( - ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, - "The resource name `-cluster-manager` must not exceed 253 characters." - ); - - format!("{}{SUFFIX}", self.cluster.name) - } -} - -// Path in opensearchproject/opensearch:3.0.0 -const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; - -struct RoleGroupBuilder<'a> { - names: &'a ContextNames, - role_name: RoleName, - cluster: ValidatedCluster, - node_config: NodeConfig, - qualified_role_group_name: String, - role_group_name: RoleGroupName, - role_group_config: OpenSearchRoleGroupConfig, -} - -impl<'a> RoleGroupBuilder<'a> { - fn new( - names: &'a ContextNames, - role_name: RoleName, - cluster: ValidatedCluster, - role_group_name: RoleGroupName, - role_group_config: OpenSearchRoleGroupConfig, - discovery_service_name: String, - ) -> RoleGroupBuilder<'a> { - // used for the name of the StatefulSet, role-group ConfigMap, ... - let qualified_role_group_name = - to_qualified_role_group_name(&cluster.name, &role_name, &role_group_name); - - RoleGroupBuilder { - names, - role_name: role_name.clone(), - cluster: cluster.clone(), - node_config: NodeConfig::new( - role_name, - cluster, - role_group_config.clone(), - discovery_service_name, - ), - qualified_role_group_name, - role_group_name, - role_group_config, - } - } - - fn build_config_map(&self) -> ConfigMap { - let metadata = ObjectMetaBuilder::new() - .name(self.qualified_role_group_name.clone()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.build_recommended_labels()) - .build(); - - let data = [( - CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), - self.node_config.static_opensearch_config(), - )] - .into(); - - ConfigMap { - metadata, - data: Some(data), - ..ConfigMap::default() - } - } - - fn build_statefulset(&self) -> StatefulSet { - let metadata = ObjectMetaBuilder::new() - .name(self.qualified_role_group_name.clone()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.build_recommended_labels()) - .build(); - - let statefulset_match_labels = role_group_selector( - &self.cluster, - &self.names.product_name, - &self.role_name, - &self.role_group_name, - ); - - let template = self.build_pod_template(); - - let data_volume_claim_template = self - .role_group_config - .config - .resources - .storage - .data - // TODO Compare name with Helm chart - .build_pvc(DATA_VOLUME_NAME, Some(vec!["ReadWriteOnce"])); - - let spec = StatefulSetSpec { - // Order does not matter for OpenSearch - pod_management_policy: Some("Parallel".to_string()), - replicas: Some(self.role_group_config.replicas.into()), - selector: LabelSelector { - match_labels: Some(statefulset_match_labels.into()), - ..LabelSelector::default() - }, - service_name: None, - template, - volume_claim_templates: Some(vec![data_volume_claim_template]), - ..StatefulSetSpec::default() - }; - - StatefulSet { - metadata, - spec: Some(spec), - status: None, - } - } - - fn build_pod_template(&self) -> PodTemplateSpec { - let mut builder = PodBuilder::new(); - - let mut node_role_labels = Labels::new(); - for node_role in self.role_group_config.config.node_roles.iter() { - node_role_labels - .insert(Label::try_from((format!("{node_role}"), "true".to_string())).unwrap()); - } - - let metadata = ObjectMetaBuilder::new() - .with_labels(self.build_recommended_labels()) - .with_labels(node_role_labels) - .build(); - - let container = self.build_container(&self.role_group_config); - - let mut pod_template = builder - .metadata(metadata) - .add_container(container) - .add_volume(Volume { - name: CONFIG_VOLUME_NAME.to_owned(), - config_map: Some(ConfigMapVolumeSource { - name: self.qualified_role_group_name.clone(), - ..Default::default() - }), - ..Default::default() - }) - .expect("The volume names are statically defined and there should be no duplicates.") - .build_template(); - - pod_template.merge_from(self.role_group_config.pod_overrides.clone()); - - pod_template - } - - fn build_container(&self, role_group_config: &OpenSearchRoleGroupConfig) -> Container { - let product_image = self - .cluster - .image - .resolve("opensearch", crate::built_info::PKG_VERSION); - - // Probe values taken from the official Helm chart - let startup_probe = Probe { - failure_threshold: Some(30), - initial_delay_seconds: Some(5), - period_seconds: Some(10), - tcp_socket: Some(TCPSocketAction { - port: IntOrString::String(HTTP_PORT_NAME.to_owned()), - ..TCPSocketAction::default() - }), - timeout_seconds: Some(3), - ..Probe::default() - }; - let readiness_probe = Probe { - failure_threshold: Some(3), - period_seconds: Some(5), - tcp_socket: Some(TCPSocketAction { - port: IntOrString::String(HTTP_PORT_NAME.to_owned()), - ..TCPSocketAction::default() - }), - timeout_seconds: Some(3), - ..Probe::default() - }; - - ContainerBuilder::new("opensearch") - .expect("should be a valid container name") - .image_from_product_image(&product_image) - .command(vec![format!( - "{OPENSEARCH_BASE_PATH}/opensearch-docker-entrypoint.sh" - )]) - .args(role_group_config.cli_overrides_to_vec()) - .add_env_vars(self.node_config.environment_variables().into()) - .add_volume_mounts([ - VolumeMount { - mount_path: format!( - "{OPENSEARCH_BASE_PATH}/config/{CONFIGURATION_FILE_OPENSEARCH_YML}" - ), - name: CONFIG_VOLUME_NAME.to_owned(), - read_only: Some(true), - sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), - ..VolumeMount::default() - }, - VolumeMount { - mount_path: format!("{OPENSEARCH_BASE_PATH}/data"), - name: DATA_VOLUME_NAME.to_owned(), - ..VolumeMount::default() - }, - ]) - .expect("The mount paths are statically defined and there should be no duplicates.") - .add_container_ports(vec![ - ContainerPort { - name: Some(HTTP_PORT_NAME.to_owned()), - container_port: HTTP_PORT.into(), - ..ContainerPort::default() - }, - ContainerPort { - name: Some(TRANSPORT_PORT_NAME.to_owned()), - container_port: TRANSPORT_PORT.into(), - ..ContainerPort::default() - }, - ContainerPort { - name: Some(METRICS_PORT_NAME.to_owned()), - container_port: METRICS_PORT.into(), - ..ContainerPort::default() - }, - ]) - .resources(self.role_group_config.config.resources.clone().into()) - .startup_probe(startup_probe) - .readiness_probe(readiness_probe) - .build() - } - - fn build_recommended_labels(&self) -> Labels { - recommended_labels( - &self.cluster, - &self.names.product_name, - &self.cluster.product_version, - &self.names.operator_name, - &self.names.controller_name, - &self.role_name, - &self.role_group_name, - ) - } - - fn build_service(&self) -> Service { - let ports = vec![ - ServicePort { - name: Some(HTTP_PORT_NAME.to_owned()), - port: HTTP_PORT.into(), - ..ServicePort::default() - }, - ServicePort { - name: Some(TRANSPORT_PORT_NAME.to_owned()), - port: TRANSPORT_PORT.into(), - ..ServicePort::default() - }, - ServicePort { - name: Some(METRICS_PORT_NAME.to_owned()), - port: METRICS_PORT.into(), - ..ServicePort::default() - }, - ]; - - // TODO Add Prometheus label - - let metadata = ObjectMetaBuilder::new() - .name(self.qualified_role_group_name.clone()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.build_recommended_labels()) - .build(); - - let service_selector = role_group_selector( - &self.cluster, - &self.names.product_name, - &self.role_name, - &self.role_group_name, - ); - - let service_spec = ServiceSpec { - // Internal communication does not need to be exposed - type_: Some("ClusterIP".to_string()), - cluster_ip: Some("None".to_string()), - ports: Some(ports), - selector: Some(service_selector.into()), - publish_not_ready_addresses: Some(true), - ..ServiceSpec::default() - }; - - Service { - metadata, - spec: Some(service_spec), - status: None, - } - } -} +pub mod node_config; +pub mod role_builder; +pub mod role_group_builder; pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> Resources { let mut config_maps = vec![]; diff --git a/rust/operator-binary/src/controller/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs similarity index 98% rename from rust/operator-binary/src/controller/node_config.rs rename to rust/operator-binary/src/controller/build/node_config.rs index ad75951..d33eb61 100644 --- a/rust/operator-binary/src/controller/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -1,10 +1,9 @@ -// TODO Create build module and move build.rs and this file into it - use serde_json::{Value, json}; use stackable_operator::builder::pod::container::FieldPathEnvVar; -use super::{OpenSearchRoleGroupConfig, ValidatedCluster}; +use super::ValidatedCluster; use crate::{ + controller::OpenSearchRoleGroupConfig, crd::v1alpha1, framework::{RoleName, builder::pod::container::EnvVarSet, to_qualified_role_group_name}, }; diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs new file mode 100644 index 0000000..dd5ba93 --- /dev/null +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -0,0 +1,176 @@ +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + k8s_openapi::api::{ + core::v1::{Service, ServicePort, ServiceSpec}, + policy::v1::PodDisruptionBudget, + }, + kvp::{ + Label, Labels, + consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, + }, +}; + +use super::role_group_builder::{ + HTTP_PORT, HTTP_PORT_NAME, METRICS_PORT, METRICS_PORT_NAME, RoleGroupBuilder, TRANSPORT_PORT, + TRANSPORT_PORT_NAME, +}; +use crate::{ + controller::{ContextNames, ValidatedCluster}, + crd::v1alpha1, + framework::{ + ClusterName, OBJECT_NAME_MAX_LENGTH, RoleName, + builder::{ + meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, + }, + }, +}; + +const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; + +pub struct RoleBuilder<'a> { + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, +} + +impl<'a> RoleBuilder<'a> { + pub fn new( + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, + ) -> RoleBuilder<'a> { + RoleBuilder { + names, + role_name: role_name.clone(), + cluster: cluster.clone(), + } + } + + pub fn role_group_builders(&self) -> Vec { + self.cluster + .role_group_configs + .iter() + .map(|(role_group_name, role_group_config)| { + RoleGroupBuilder::new( + self.names, + self.role_name.clone(), + self.cluster.clone(), + role_group_name.clone(), + role_group_config.clone(), + self.discovery_service_name(), + ) + }) + .collect() + } + + pub fn build_cluster_manager_service(&self) -> Service { + let ports = vec![ + ServicePort { + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(METRICS_PORT_NAME.to_owned()), + port: METRICS_PORT.into(), + ..ServicePort::default() + }, + ]; + + // Well-known Kubernetes labels + let mut labels = Labels::role_selector( + &self.cluster, + &self.names.product_name.to_string(), + &self.role_name.to_string(), + ) + .unwrap(); + + let managed_by = Label::managed_by( + &self.names.operator_name.to_string(), + &self.names.controller_name.to_string(), + ) + .unwrap(); + let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); + + labels.insert(managed_by); + labels.insert(version); + + // Stackable-specific labels + labels + .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) + .unwrap(); + + let metadata = ObjectMetaBuilder::new() + .name(self.discovery_service_name()) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(labels) + .build(); + + let service_selector = [( + v1alpha1::NodeRole::ClusterManager.to_string(), + "true".to_owned(), + )] + .into(); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(service_selector), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, + } + } + + pub fn build_pdb(&self) -> Option { + let pdb_config = &self.cluster.role_config.pod_disruption_budget; + + if pdb_config.enabled { + let max_unavailable = pdb_config + .max_unavailable + .unwrap_or(PDB_DEFAULT_MAX_UNAVAILABLE); + Some( + pod_disruption_budget_builder_with_role( + &self.cluster, + &self.names.product_name, + &self.role_name, + &self.names.operator_name, + &self.names.controller_name, + ) + .with_max_unavailable(max_unavailable) + .build(), + ) + } else { + None + } + } + + fn discovery_service_name(&self) -> String { + const SUFFIX: &str = "-cluster-manager"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, + "The resource name `-cluster-manager` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.cluster.name) + } +} diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs new file mode 100644 index 0000000..ddec54a --- /dev/null +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -0,0 +1,338 @@ +use stackable_operator::{ + builder::{ + meta::ObjectMetaBuilder, + pod::{PodBuilder, container::ContainerBuilder}, + }, + k8s_openapi::{ + DeepMerge, + api::{ + apps::v1::{StatefulSet, StatefulSetSpec}, + core::v1::{ + ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodTemplateSpec, Probe, + Service, ServicePort, ServiceSpec, TCPSocketAction, Volume, VolumeMount, + }, + }, + apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, + }, + kvp::{Label, Labels}, +}; + +use super::node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}; +use crate::{ + controller::{ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster}, + framework::{ + RoleGroupName, RoleName, + builder::meta::ownerreference_from_resource, + kvp::label::{recommended_labels, role_group_selector}, + to_qualified_role_group_name, + }, +}; + +pub const HTTP_PORT_NAME: &str = "http"; +pub const HTTP_PORT: u16 = 9200; +pub const TRANSPORT_PORT_NAME: &str = "transport"; +pub const TRANSPORT_PORT: u16 = 9300; +pub const METRICS_PORT_NAME: &str = "metrics"; +pub const METRICS_PORT: u16 = 9600; + +const CONFIG_VOLUME_NAME: &str = "config"; +const DATA_VOLUME_NAME: &str = "data"; + +// Path in opensearchproject/opensearch:3.0.0 +const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; + +pub struct RoleGroupBuilder<'a> { + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, + node_config: NodeConfig, + qualified_role_group_name: String, + role_group_name: RoleGroupName, + role_group_config: OpenSearchRoleGroupConfig, +} + +impl<'a> RoleGroupBuilder<'a> { + pub fn new( + names: &'a ContextNames, + role_name: RoleName, + cluster: ValidatedCluster, + role_group_name: RoleGroupName, + role_group_config: OpenSearchRoleGroupConfig, + discovery_service_name: String, + ) -> RoleGroupBuilder<'a> { + // used for the name of the StatefulSet, role-group ConfigMap, ... + let qualified_role_group_name = + to_qualified_role_group_name(&cluster.name, &role_name, &role_group_name); + + RoleGroupBuilder { + names, + role_name: role_name.clone(), + cluster: cluster.clone(), + node_config: NodeConfig::new( + role_name, + cluster, + role_group_config.clone(), + discovery_service_name, + ), + qualified_role_group_name, + role_group_name, + role_group_config, + } + } + + pub fn build_config_map(&self) -> ConfigMap { + let metadata = ObjectMetaBuilder::new() + .name(self.qualified_role_group_name.clone()) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.build_recommended_labels()) + .build(); + + let data = [( + CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), + self.node_config.static_opensearch_config(), + )] + .into(); + + ConfigMap { + metadata, + data: Some(data), + ..ConfigMap::default() + } + } + + pub fn build_statefulset(&self) -> StatefulSet { + let metadata = ObjectMetaBuilder::new() + .name(self.qualified_role_group_name.clone()) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.build_recommended_labels()) + .build(); + + let statefulset_match_labels = role_group_selector( + &self.cluster, + &self.names.product_name, + &self.role_name, + &self.role_group_name, + ); + + let template = self.build_pod_template(); + + let data_volume_claim_template = self + .role_group_config + .config + .resources + .storage + .data + // TODO Compare name with Helm chart + .build_pvc(DATA_VOLUME_NAME, Some(vec!["ReadWriteOnce"])); + + let spec = StatefulSetSpec { + // Order does not matter for OpenSearch + pod_management_policy: Some("Parallel".to_string()), + replicas: Some(self.role_group_config.replicas.into()), + selector: LabelSelector { + match_labels: Some(statefulset_match_labels.into()), + ..LabelSelector::default() + }, + service_name: None, + template, + volume_claim_templates: Some(vec![data_volume_claim_template]), + ..StatefulSetSpec::default() + }; + + StatefulSet { + metadata, + spec: Some(spec), + status: None, + } + } + + fn build_pod_template(&self) -> PodTemplateSpec { + let mut builder = PodBuilder::new(); + + let mut node_role_labels = Labels::new(); + for node_role in self.role_group_config.config.node_roles.iter() { + node_role_labels + .insert(Label::try_from((format!("{node_role}"), "true".to_string())).unwrap()); + } + + let metadata = ObjectMetaBuilder::new() + .with_labels(self.build_recommended_labels()) + .with_labels(node_role_labels) + .build(); + + let container = self.build_container(&self.role_group_config); + + let mut pod_template = builder + .metadata(metadata) + .add_container(container) + .add_volume(Volume { + name: CONFIG_VOLUME_NAME.to_owned(), + config_map: Some(ConfigMapVolumeSource { + name: self.qualified_role_group_name.clone(), + ..Default::default() + }), + ..Default::default() + }) + .expect("The volume names are statically defined and there should be no duplicates.") + .build_template(); + + pod_template.merge_from(self.role_group_config.pod_overrides.clone()); + + pod_template + } + + fn build_container(&self, role_group_config: &OpenSearchRoleGroupConfig) -> Container { + let product_image = self + .cluster + .image + .resolve("opensearch", crate::built_info::PKG_VERSION); + + // Probe values taken from the official Helm chart + let startup_probe = Probe { + failure_threshold: Some(30), + initial_delay_seconds: Some(5), + period_seconds: Some(10), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::String(HTTP_PORT_NAME.to_owned()), + ..TCPSocketAction::default() + }), + timeout_seconds: Some(3), + ..Probe::default() + }; + let readiness_probe = Probe { + failure_threshold: Some(3), + period_seconds: Some(5), + tcp_socket: Some(TCPSocketAction { + port: IntOrString::String(HTTP_PORT_NAME.to_owned()), + ..TCPSocketAction::default() + }), + timeout_seconds: Some(3), + ..Probe::default() + }; + + ContainerBuilder::new("opensearch") + .expect("should be a valid container name") + .image_from_product_image(&product_image) + .command(vec![format!( + "{OPENSEARCH_BASE_PATH}/opensearch-docker-entrypoint.sh" + )]) + .args(role_group_config.cli_overrides_to_vec()) + .add_env_vars(self.node_config.environment_variables().into()) + .add_volume_mounts([ + VolumeMount { + mount_path: format!( + "{OPENSEARCH_BASE_PATH}/config/{CONFIGURATION_FILE_OPENSEARCH_YML}" + ), + name: CONFIG_VOLUME_NAME.to_owned(), + read_only: Some(true), + sub_path: Some(CONFIGURATION_FILE_OPENSEARCH_YML.to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{OPENSEARCH_BASE_PATH}/data"), + name: DATA_VOLUME_NAME.to_owned(), + ..VolumeMount::default() + }, + ]) + .expect("The mount paths are statically defined and there should be no duplicates.") + .add_container_ports(vec![ + ContainerPort { + name: Some(HTTP_PORT_NAME.to_owned()), + container_port: HTTP_PORT.into(), + ..ContainerPort::default() + }, + ContainerPort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + container_port: TRANSPORT_PORT.into(), + ..ContainerPort::default() + }, + ContainerPort { + name: Some(METRICS_PORT_NAME.to_owned()), + container_port: METRICS_PORT.into(), + ..ContainerPort::default() + }, + ]) + .resources(self.role_group_config.config.resources.clone().into()) + .startup_probe(startup_probe) + .readiness_probe(readiness_probe) + .build() + } + + fn build_recommended_labels(&self) -> Labels { + recommended_labels( + &self.cluster, + &self.names.product_name, + &self.cluster.product_version, + &self.names.operator_name, + &self.names.controller_name, + &self.role_name, + &self.role_group_name, + ) + } + + pub fn build_service(&self) -> Service { + let ports = vec![ + ServicePort { + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(METRICS_PORT_NAME.to_owned()), + port: METRICS_PORT.into(), + ..ServicePort::default() + }, + ]; + + // TODO Add Prometheus label + + let metadata = ObjectMetaBuilder::new() + .name(self.qualified_role_group_name.clone()) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.build_recommended_labels()) + .build(); + + let service_selector = role_group_selector( + &self.cluster, + &self.names.product_name, + &self.role_name, + &self.role_group_name, + ); + + let service_spec = ServiceSpec { + // Internal communication does not need to be exposed + type_: Some("ClusterIP".to_string()), + cluster_ip: Some("None".to_string()), + ports: Some(ports), + selector: Some(service_selector.into()), + publish_not_ready_addresses: Some(true), + ..ServiceSpec::default() + }; + + Service { + metadata, + spec: Some(service_spec), + status: None, + } + } +} From b764b13df0f8733e16202cfeeff900fa0d741255 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 10 Jul 2025 12:26:38 +0200 Subject: [PATCH 23/35] Add ServiceAccount and RoleBinding --- .../opensearch-operator/templates/roles.yaml | 2 - rust/operator-binary/src/controller.rs | 5 +- rust/operator-binary/src/controller/apply.rs | 6 + rust/operator-binary/src/controller/build.rs | 6 + .../src/controller/build/role_builder.rs | 140 +++++++++++++++--- .../controller/build/role_group_builder.rs | 13 +- tests/templates/kuttl/smoke/00-patch-ns.yaml | 15 ++ tests/templates/kuttl/smoke/01-rbac.yaml | 31 ++++ .../templates/kuttl/smoke/02-limit-range.yaml | 11 ++ .../kuttl/smoke/20-test-opensearch.yaml | 19 ++- 10 files changed, 219 insertions(+), 29 deletions(-) create mode 100644 tests/templates/kuttl/smoke/00-patch-ns.yaml create mode 100644 tests/templates/kuttl/smoke/01-rbac.yaml create mode 100644 tests/templates/kuttl/smoke/02-limit-range.yaml diff --git a/deploy/helm/opensearch-operator/templates/roles.yaml b/deploy/helm/opensearch-operator/templates/roles.yaml index 2a1115c..efb1767 100644 --- a/deploy/helm/opensearch-operator/templates/roles.yaml +++ b/deploy/helm/opensearch-operator/templates/roles.yaml @@ -122,7 +122,6 @@ rules: - events verbs: - create -{{ if .Capabilities.APIVersions.Has "security.openshift.io/v1" }} - apiGroups: - security.openshift.io resources: @@ -131,4 +130,3 @@ rules: - nonroot-v2 verbs: - use -{{ end }} diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 16c4c26..f563c90 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -8,8 +8,9 @@ use stackable_operator::{ commons::product_image_selection::ProductImage, k8s_openapi::api::{ apps::v1::StatefulSet, - core::v1::{ConfigMap, Service}, + core::v1::{ConfigMap, Service, ServiceAccount}, policy::v1::PodDisruptionBudget, + rbac::v1::RoleBinding, }, kube::{Resource, api::ObjectMeta, core::DeserializeGuard, runtime::controller::Action}, logging::controller::ReconcilerError, @@ -258,6 +259,8 @@ struct Resources { stateful_sets: Vec, services: Vec, config_maps: Vec, + service_accounts: Vec, + role_bindings: Vec, pod_disruption_budgets: Vec, status: PhantomData, } diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 152ec1d..57985f5 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -61,6 +61,10 @@ impl<'a> Applier<'a> { let config_maps = self.add_resources(resources.config_maps).await?; + let service_accounts = self.add_resources(resources.service_accounts).await?; + + let role_bindings = self.add_resources(resources.role_bindings).await?; + let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?; self.cluster_resources @@ -72,6 +76,8 @@ impl<'a> Applier<'a> { stateful_sets, services, config_maps, + service_accounts, + role_bindings, pod_disruption_budgets, status: PhantomData, }) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index fb9d6c9..6d4fd38 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -27,12 +27,18 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> Resources RoleBuilder<'a> { } } + // TODO Only one builder function which calls the other ones? + pub fn role_group_builders(&self) -> Vec { self.cluster .role_group_configs @@ -54,6 +60,7 @@ impl<'a> RoleBuilder<'a> { RoleGroupBuilder::new( self.names, self.role_name.clone(), + self.service_account_name(), self.cluster.clone(), role_group_name.clone(), role_group_config.clone(), @@ -63,24 +70,57 @@ impl<'a> RoleBuilder<'a> { .collect() } - pub fn build_cluster_manager_service(&self) -> Service { - let ports = vec![ - ServicePort { - name: Some(HTTP_PORT_NAME.to_owned()), - port: HTTP_PORT.into(), - ..ServicePort::default() - }, - ServicePort { - name: Some(TRANSPORT_PORT_NAME.to_owned()), - port: TRANSPORT_PORT.into(), - ..ServicePort::default() - }, - ServicePort { - name: Some(METRICS_PORT_NAME.to_owned()), - port: METRICS_PORT.into(), - ..ServicePort::default() + pub fn build_service_account(&self) -> ServiceAccount { + // TODO Move to a common create_meta function + let metadata = ObjectMetaBuilder::new() + .name(self.service_account_name()) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.labels()) + .build(); + + ServiceAccount { + metadata, + ..ServiceAccount::default() + } + } + + pub fn build_role_binding(&self) -> RoleBinding { + // TODO Move to a common create_meta function + let metadata = ObjectMetaBuilder::new() + .name(self.role_binding_name()) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.labels()) + .build(); + + RoleBinding { + metadata, + role_ref: RoleRef { + api_group: ClusterRole::GROUP.to_owned(), + kind: ClusterRole::KIND.to_owned(), + name: self.cluster_role_name(), }, - ]; + subjects: Some(vec![Subject { + api_group: Some(ServiceAccount::GROUP.to_owned()), + kind: ServiceAccount::KIND.to_owned(), + name: self.service_account_name(), + namespace: Some(self.cluster.namespace.clone()), + }]), + } + } + + /// Labels on role resources + fn labels(&self) -> Labels { + // TODO Are the labels stackable.tech/name and stackable.tech/instance missing? // Well-known Kubernetes labels let mut labels = Labels::role_selector( @@ -105,6 +145,28 @@ impl<'a> RoleBuilder<'a> { .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) .unwrap(); + labels + } + + pub fn build_cluster_manager_service(&self) -> Service { + let ports = vec![ + ServicePort { + name: Some(HTTP_PORT_NAME.to_owned()), + port: HTTP_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(TRANSPORT_PORT_NAME.to_owned()), + port: TRANSPORT_PORT.into(), + ..ServicePort::default() + }, + ServicePort { + name: Some(METRICS_PORT_NAME.to_owned()), + port: METRICS_PORT.into(), + ..ServicePort::default() + }, + ]; + let metadata = ObjectMetaBuilder::new() .name(self.discovery_service_name()) .namespace(&self.cluster.namespace) @@ -113,7 +175,7 @@ impl<'a> RoleBuilder<'a> { None, Some(true), )) - .with_labels(labels) + .with_labels(self.labels()) .build(); let service_selector = [( @@ -162,6 +224,42 @@ impl<'a> RoleBuilder<'a> { } } + fn service_account_name(&self) -> String { + const SUFFIX: &str = "-serviceaccount"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, + "The resource name `-serviceaccount` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.cluster.name) + } + + fn role_binding_name(&self) -> String { + const SUFFIX: &str = "-rolebinding"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, + "The resource name `-rolebinding` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.cluster.name) + } + + fn cluster_role_name(&self) -> String { + const SUFFIX: &str = "-clusterrole"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, + "The resource name `-clusterrole` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.names.product_name) + } + fn discovery_service_name(&self) -> String { const SUFFIX: &str = "-cluster-manager"; diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index ddec54a..851e535 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -8,8 +8,9 @@ use stackable_operator::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ - ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodTemplateSpec, Probe, - Service, ServicePort, ServiceSpec, TCPSocketAction, Volume, VolumeMount, + ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodSecurityContext, + PodTemplateSpec, Probe, Service, ServicePort, ServiceSpec, TCPSocketAction, Volume, + VolumeMount, }, }, apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, @@ -44,6 +45,7 @@ const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; pub struct RoleGroupBuilder<'a> { names: &'a ContextNames, role_name: RoleName, + service_account_name: String, cluster: ValidatedCluster, node_config: NodeConfig, qualified_role_group_name: String, @@ -55,6 +57,7 @@ impl<'a> RoleGroupBuilder<'a> { pub fn new( names: &'a ContextNames, role_name: RoleName, + service_account_name: String, cluster: ValidatedCluster, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, @@ -67,6 +70,7 @@ impl<'a> RoleGroupBuilder<'a> { RoleGroupBuilder { names, role_name: role_name.clone(), + service_account_name, cluster: cluster.clone(), node_config: NodeConfig::new( role_name, @@ -184,6 +188,11 @@ impl<'a> RoleGroupBuilder<'a> { ..Default::default() }) .expect("The volume names are statically defined and there should be no duplicates.") + .security_context(PodSecurityContext { + fs_group: Some(1000), + ..PodSecurityContext::default() + }) + .service_account_name(&self.service_account_name) .build_template(); pod_template.merge_from(self.role_group_config.pod_overrides.clone()); diff --git a/tests/templates/kuttl/smoke/00-patch-ns.yaml b/tests/templates/kuttl/smoke/00-patch-ns.yaml new file mode 100644 index 0000000..d4f91fa --- /dev/null +++ b/tests/templates/kuttl/smoke/00-patch-ns.yaml @@ -0,0 +1,15 @@ +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl patch namespace $NAMESPACE --patch=' + { + "metadata": { + "labels": { + "pod-security.kubernetes.io/enforce": "privileged" + } + } + }' + timeout: 120 diff --git a/tests/templates/kuttl/smoke/01-rbac.yaml b/tests/templates/kuttl/smoke/01-rbac.yaml new file mode 100644 index 0000000..64eced8 --- /dev/null +++ b/tests/templates/kuttl/smoke/01-rbac.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-service-account +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role +rules: + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - privileged + verbs: + - use +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role-binding +subjects: + - kind: ServiceAccount + name: test-service-account +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: test-role diff --git a/tests/templates/kuttl/smoke/02-limit-range.yaml b/tests/templates/kuttl/smoke/02-limit-range.yaml new file mode 100644 index 0000000..b1789b2 --- /dev/null +++ b/tests/templates/kuttl/smoke/02-limit-range.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: LimitRange +metadata: + name: limit-request-ratio +spec: + limits: + - type: Container + maxLimitRequestRatio: + cpu: 5 + memory: 1 diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml index 4d9fcff..d5a086b 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml @@ -6,12 +6,13 @@ metadata: spec: template: spec: - # serviceAccountName: test-sa containers: - name: test-opensearch image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev command: - /bin/bash + - -euxo + - pipefail - -c args: - | @@ -26,6 +27,19 @@ spec: mountPath: /stackable/scripts - name: tls mountPath: /stackable/tls + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 128Mi + cpu: 400m volumes: - name: script configMap: @@ -43,10 +57,9 @@ spec: resources: requests: storage: "1" + serviceAccountName: test-service-account securityContext: fsGroup: 1000 - runAsGroup: 1000 - runAsUser: 1000 restartPolicy: OnFailure --- apiVersion: v1 From 4f300214fba121ae4dd7fd3c03c35a60fc4f7aa3 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 11 Jul 2025 17:27:22 +0200 Subject: [PATCH 24/35] Improve code quality and fix a lot of minor things --- rust/operator-binary/src/controller/build.rs | 6 +- .../src/controller/build/node_config.rs | 17 +- .../src/controller/build/role_builder.rs | 177 ++++++------------ .../controller/build/role_group_builder.rs | 171 ++++++++--------- rust/operator-binary/src/framework.rs | 66 ++----- .../src/framework/cluster_resources.rs | 4 +- .../src/framework/kvp/label.rs | 2 +- .../src/framework/role_group_utils.rs | 92 +++++++++ .../src/framework/role_utils.rs | 48 +++++ tests/templates/kuttl/smoke/10-assert.yaml | 93 +++++---- .../kuttl/smoke/10-install-opensearch.yaml | 2 - .../kuttl/smoke/20-test-opensearch.yaml | 2 +- 12 files changed, 363 insertions(+), 317 deletions(-) create mode 100644 rust/operator-binary/src/framework/role_group_utils.rs diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 6d4fd38..5b88026 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -16,12 +16,12 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> Resources { - names: &'a ContextNames, role_name: RoleName, cluster: ValidatedCluster, + context_names: &'a ContextNames, + resource_names: ResourceNames, } impl<'a> RoleBuilder<'a> { pub fn new( - names: &'a ContextNames, role_name: RoleName, cluster: ValidatedCluster, + context_names: &'a ContextNames, ) -> RoleBuilder<'a> { RoleBuilder { - names, role_name: role_name.clone(), cluster: cluster.clone(), + context_names, + resource_names: ResourceNames { + cluster_name: cluster.name.clone(), + product_name: context_names.product_name.clone(), + }, } } @@ -58,30 +64,20 @@ impl<'a> RoleBuilder<'a> { .iter() .map(|(role_group_name, role_group_config)| { RoleGroupBuilder::new( - self.names, self.role_name.clone(), - self.service_account_name(), + self.resource_names.service_account_name(), self.cluster.clone(), role_group_name.clone(), role_group_config.clone(), - self.discovery_service_name(), + self.context_names, + self.resource_names.discovery_service_name(), ) }) .collect() } pub fn build_service_account(&self) -> ServiceAccount { - // TODO Move to a common create_meta function - let metadata = ObjectMetaBuilder::new() - .name(self.service_account_name()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.labels()) - .build(); + let metadata = self.common_metadata(self.resource_names.service_account_name()); ServiceAccount { metadata, @@ -90,64 +86,24 @@ impl<'a> RoleBuilder<'a> { } pub fn build_role_binding(&self) -> RoleBinding { - // TODO Move to a common create_meta function - let metadata = ObjectMetaBuilder::new() - .name(self.role_binding_name()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.labels()) - .build(); + let metadata = self.common_metadata(self.resource_names.role_binding_name()); RoleBinding { metadata, role_ref: RoleRef { api_group: ClusterRole::GROUP.to_owned(), kind: ClusterRole::KIND.to_owned(), - name: self.cluster_role_name(), + name: self.resource_names.cluster_role_name(), }, subjects: Some(vec![Subject { api_group: Some(ServiceAccount::GROUP.to_owned()), kind: ServiceAccount::KIND.to_owned(), - name: self.service_account_name(), + name: self.resource_names.service_account_name(), namespace: Some(self.cluster.namespace.clone()), }]), } } - /// Labels on role resources - fn labels(&self) -> Labels { - // TODO Are the labels stackable.tech/name and stackable.tech/instance missing? - - // Well-known Kubernetes labels - let mut labels = Labels::role_selector( - &self.cluster, - &self.names.product_name.to_string(), - &self.role_name.to_string(), - ) - .unwrap(); - - let managed_by = Label::managed_by( - &self.names.operator_name.to_string(), - &self.names.controller_name.to_string(), - ) - .unwrap(); - let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); - - labels.insert(managed_by); - labels.insert(version); - - // Stackable-specific labels - labels - .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) - .unwrap(); - - labels - } - pub fn build_cluster_manager_service(&self) -> Service { let ports = vec![ ServicePort { @@ -160,23 +116,9 @@ impl<'a> RoleBuilder<'a> { port: TRANSPORT_PORT.into(), ..ServicePort::default() }, - ServicePort { - name: Some(METRICS_PORT_NAME.to_owned()), - port: METRICS_PORT.into(), - ..ServicePort::default() - }, ]; - let metadata = ObjectMetaBuilder::new() - .name(self.discovery_service_name()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.labels()) - .build(); + let metadata = self.common_metadata(self.resource_names.discovery_service_name()); let service_selector = [( v1alpha1::NodeRole::ClusterManager.to_string(), @@ -211,10 +153,10 @@ impl<'a> RoleBuilder<'a> { Some( pod_disruption_budget_builder_with_role( &self.cluster, - &self.names.product_name, + &self.context_names.product_name, &self.role_name, - &self.names.operator_name, - &self.names.controller_name, + &self.context_names.operator_name, + &self.context_names.controller_name, ) .with_max_unavailable(max_unavailable) .build(), @@ -224,51 +166,44 @@ impl<'a> RoleBuilder<'a> { } } - fn service_account_name(&self) -> String { - const SUFFIX: &str = "-serviceaccount"; - - // Compile-time check - const _: () = assert!( - ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, - "The resource name `-serviceaccount` must not exceed 253 characters." - ); - - format!("{}{SUFFIX}", self.cluster.name) - } - - fn role_binding_name(&self) -> String { - const SUFFIX: &str = "-rolebinding"; - - // Compile-time check - const _: () = assert!( - ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, - "The resource name `-rolebinding` must not exceed 253 characters." - ); - - format!("{}{SUFFIX}", self.cluster.name) + fn common_metadata(&self, resource_name: impl Into) -> ObjectMeta { + ObjectMetaBuilder::new() + .name(resource_name) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.labels()) + .build() } - fn cluster_role_name(&self) -> String { - const SUFFIX: &str = "-clusterrole"; - - // Compile-time check - const _: () = assert!( - ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, - "The resource name `-clusterrole` must not exceed 253 characters." - ); + /// Labels on role resources + fn labels(&self) -> Labels { + // Well-known Kubernetes labels + let mut labels = Labels::role_selector( + &self.cluster, + &self.context_names.product_name.to_string(), + &self.role_name.to_string(), + ) + .unwrap(); - format!("{}{SUFFIX}", self.names.product_name) - } + let managed_by = Label::managed_by( + &self.context_names.operator_name.to_string(), + &self.context_names.controller_name.to_string(), + ) + .unwrap(); + let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); - fn discovery_service_name(&self) -> String { - const SUFFIX: &str = "-cluster-manager"; + labels.insert(managed_by); + labels.insert(version); - // Compile-time check - const _: () = assert!( - ClusterName::MAX_LENGTH + SUFFIX.len() <= OBJECT_NAME_MAX_LENGTH, - "The resource name `-cluster-manager` must not exceed 253 characters." - ); + // Stackable-specific labels + labels + .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) + .unwrap(); - format!("{}{SUFFIX}", self.cluster.name) + labels } } diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 851e535..938b920 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -15,6 +15,7 @@ use stackable_operator::{ }, apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, }, + kube::api::ObjectMeta, kvp::{Label, Labels}, }; @@ -25,7 +26,7 @@ use crate::{ RoleGroupName, RoleName, builder::meta::ownerreference_from_resource, kvp::label::{recommended_labels, role_group_selector}, - to_qualified_role_group_name, + role_group_utils::ResourceNames, }, }; @@ -33,8 +34,6 @@ pub const HTTP_PORT_NAME: &str = "http"; pub const HTTP_PORT: u16 = 9200; pub const TRANSPORT_PORT_NAME: &str = "transport"; pub const TRANSPORT_PORT: u16 = 9300; -pub const METRICS_PORT_NAME: &str = "metrics"; -pub const METRICS_PORT: u16 = 9600; const CONFIG_VOLUME_NAME: &str = "config"; const DATA_VOLUME_NAME: &str = "data"; @@ -43,58 +42,50 @@ const DATA_VOLUME_NAME: &str = "data"; const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; pub struct RoleGroupBuilder<'a> { - names: &'a ContextNames, role_name: RoleName, service_account_name: String, cluster: ValidatedCluster, node_config: NodeConfig, - qualified_role_group_name: String, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, + context_names: &'a ContextNames, + resource_names: ResourceNames, } impl<'a> RoleGroupBuilder<'a> { pub fn new( - names: &'a ContextNames, role_name: RoleName, service_account_name: String, cluster: ValidatedCluster, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, + context_names: &'a ContextNames, discovery_service_name: String, ) -> RoleGroupBuilder<'a> { - // used for the name of the StatefulSet, role-group ConfigMap, ... - let qualified_role_group_name = - to_qualified_role_group_name(&cluster.name, &role_name, &role_group_name); - RoleGroupBuilder { - names, role_name: role_name.clone(), service_account_name, cluster: cluster.clone(), node_config: NodeConfig::new( - role_name, - cluster, + role_name.clone(), + cluster.clone(), role_group_config.clone(), discovery_service_name, ), - qualified_role_group_name, - role_group_name, + role_group_name: role_group_name.clone(), role_group_config, + context_names, + resource_names: ResourceNames { + cluster_name: cluster.name.clone(), + role_name, + role_group_name, + }, } } pub fn build_config_map(&self) -> ConfigMap { - let metadata = ObjectMetaBuilder::new() - .name(self.qualified_role_group_name.clone()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.build_recommended_labels()) - .build(); + let metadata = + self.common_metadata(self.resource_names.role_group_config_map(), Labels::new()); let data = [( CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), @@ -109,24 +100,8 @@ impl<'a> RoleGroupBuilder<'a> { } } - pub fn build_statefulset(&self) -> StatefulSet { - let metadata = ObjectMetaBuilder::new() - .name(self.qualified_role_group_name.clone()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.build_recommended_labels()) - .build(); - - let statefulset_match_labels = role_group_selector( - &self.cluster, - &self.names.product_name, - &self.role_name, - &self.role_group_name, - ); + pub fn build_stateful_set(&self) -> StatefulSet { + let metadata = self.common_metadata(self.resource_names.stateful_set_name(), Labels::new()); let template = self.build_pod_template(); @@ -144,10 +119,10 @@ impl<'a> RoleGroupBuilder<'a> { pod_management_policy: Some("Parallel".to_string()), replicas: Some(self.role_group_config.replicas.into()), selector: LabelSelector { - match_labels: Some(statefulset_match_labels.into()), + match_labels: Some(self.pod_selector().into()), ..LabelSelector::default() }, - service_name: None, + service_name: Some(self.resource_names.headless_service_name()), template, volume_claim_templates: Some(vec![data_volume_claim_template]), ..StatefulSetSpec::default() @@ -165,12 +140,14 @@ impl<'a> RoleGroupBuilder<'a> { let mut node_role_labels = Labels::new(); for node_role in self.role_group_config.config.node_roles.iter() { - node_role_labels - .insert(Label::try_from((format!("{node_role}"), "true".to_string())).unwrap()); + node_role_labels.insert( + Label::try_from((format!("{node_role}"), "true".to_string())) + .expect("should be a valid label"), + ); } let metadata = ObjectMetaBuilder::new() - .with_labels(self.build_recommended_labels()) + .with_labels(self.recommended_labels()) .with_labels(node_role_labels) .build(); @@ -182,7 +159,7 @@ impl<'a> RoleGroupBuilder<'a> { .add_volume(Volume { name: CONFIG_VOLUME_NAME.to_owned(), config_map: Some(ConfigMapVolumeSource { - name: self.qualified_role_group_name.clone(), + name: self.resource_names.role_group_config_map(), ..Default::default() }), ..Default::default() @@ -265,11 +242,6 @@ impl<'a> RoleGroupBuilder<'a> { container_port: TRANSPORT_PORT.into(), ..ContainerPort::default() }, - ContainerPort { - name: Some(METRICS_PORT_NAME.to_owned()), - container_port: METRICS_PORT.into(), - ..ContainerPort::default() - }, ]) .resources(self.role_group_config.config.resources.clone().into()) .startup_probe(startup_probe) @@ -277,19 +249,7 @@ impl<'a> RoleGroupBuilder<'a> { .build() } - fn build_recommended_labels(&self) -> Labels { - recommended_labels( - &self.cluster, - &self.names.product_name, - &self.cluster.product_version, - &self.names.operator_name, - &self.names.controller_name, - &self.role_name, - &self.role_group_name, - ) - } - - pub fn build_service(&self) -> Service { + pub fn build_headless_service(&self) -> Service { let ports = vec![ ServicePort { name: Some(HTTP_PORT_NAME.to_owned()), @@ -301,39 +261,29 @@ impl<'a> RoleGroupBuilder<'a> { port: TRANSPORT_PORT.into(), ..ServicePort::default() }, - ServicePort { - name: Some(METRICS_PORT_NAME.to_owned()), - port: METRICS_PORT.into(), - ..ServicePort::default() - }, ]; - // TODO Add Prometheus label - - let metadata = ObjectMetaBuilder::new() - .name(self.qualified_role_group_name.clone()) - .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) - .with_labels(self.build_recommended_labels()) - .build(); + self.build_role_group_service( + self.resource_names.headless_service_name(), + ports, + Labels::new(), + ) + } - let service_selector = role_group_selector( - &self.cluster, - &self.names.product_name, - &self.role_name, - &self.role_group_name, - ); + fn build_role_group_service( + &self, + service_name: impl Into, + ports: Vec, + extra_labels: Labels, + ) -> Service { + let metadata = self.common_metadata(service_name, extra_labels); let service_spec = ServiceSpec { // Internal communication does not need to be exposed type_: Some("ClusterIP".to_string()), cluster_ip: Some("None".to_string()), ports: Some(ports), - selector: Some(service_selector.into()), + selector: Some(self.pod_selector().into()), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() }; @@ -344,4 +294,43 @@ impl<'a> RoleGroupBuilder<'a> { status: None, } } + + fn common_metadata( + &self, + resource_name: impl Into, + extra_labels: Labels, + ) -> ObjectMeta { + ObjectMetaBuilder::new() + .name(resource_name) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.recommended_labels()) + .with_labels(extra_labels) + .build() + } + + fn recommended_labels(&self) -> Labels { + recommended_labels( + &self.cluster, + &self.context_names.product_name, + &self.cluster.product_version, + &self.context_names.operator_name, + &self.context_names.controller_name, + &self.role_name, + &self.role_group_name, + ) + } + + fn pod_selector(&self) -> Labels { + role_group_selector( + &self.cluster, + &self.context_names.product_name, + &self.role_name, + &self.role_group_name, + ) + } } diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 552d631..aad601d 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -1,9 +1,10 @@ // Type-safe wrappers that cannot throw errors // The point is, to move the validation "upwards". +// The contents of this module will be moved to operator-rs when stabilized. use std::{fmt::Display, str::FromStr}; -use kvp::label::LABEL_VALUE_MAX_LENGTH; +use kvp::label::MAX_LABEL_VALUE_LENGTH; use snafu::{ResultExt, Snafu, ensure}; use stackable_operator::kvp::LabelValue; use strum::{EnumDiscriminants, IntoStaticStr}; @@ -11,6 +12,7 @@ use strum::{EnumDiscriminants, IntoStaticStr}; pub mod builder; pub mod cluster_resources; pub mod kvp; +pub mod role_group_utils; pub mod role_utils; #[derive(Snafu, Debug, EnumDiscriminants)] @@ -30,9 +32,10 @@ pub enum Error { }, } +// TODO The maximum length of objects differs. /// Maximum length of a DNS subdomain name as defined in RFC 1123. #[allow(dead_code)] -pub const OBJECT_NAME_MAX_LENGTH: usize = 253; +pub const MAX_OBJECT_NAME_LENGTH: usize = 253; /// Has a name that can be used as a DNS subdomain name as defined in RFC 1123. /// Most resource types, e.g. a Pod, require such a compliant name. @@ -130,7 +133,7 @@ attributed_string_type! { attributed_string_type! { ProductVersion, "The version of a product, e.g. \"3.0.0\"", - (max_length = LABEL_VALUE_MAX_LENGTH), + (max_length = MAX_LABEL_VALUE_LENGTH), is_valid_label_value } attributed_string_type! { @@ -138,72 +141,41 @@ attributed_string_type! { "The name of a cluster/stacklet, e.g. \"my-opensearch-cluster\"", // Suffixes are added to produce a resource names. According compile-time check ensures that // max_length cannot be set higher. - (max_length = LABEL_VALUE_MAX_LENGTH), + (max_length = 24), is_object_name, is_valid_label_value } attributed_string_type! { ControllerName, "The name of a controller in an operator, e.g. \"opensearchcluster\"", - (max_length = LABEL_VALUE_MAX_LENGTH), + (max_length = MAX_LABEL_VALUE_LENGTH), is_valid_label_value } attributed_string_type! { OperatorName, "The name of an operator, e.g. \"opensearch.stackable.tech\"", - (max_length = LABEL_VALUE_MAX_LENGTH), + (max_length = MAX_LABEL_VALUE_LENGTH), is_valid_label_value } attributed_string_type! { RoleGroupName, - "The name of a role-group name, e.g. \"clusterManager\"", - (max_length = LABEL_VALUE_MAX_LENGTH), + "The name of a role-group name, e.g. \"cluster-manager\"", + (max_length = 16), is_object_name, is_valid_label_value } attributed_string_type! { RoleName, "The name of a role name, e.g. \"nodes\"", - (max_length = LABEL_VALUE_MAX_LENGTH), + (max_length = 10), is_object_name, is_valid_label_value } -/// Creates a qualified role group name consisting of the cluster name, role name and role-group -/// name. -/// The result is a valid DNS subdomain name as defined in RFC 1123 that can be used e.g. as a name -/// for a StatefulSet. -pub fn to_qualified_role_group_name( - cluster_name: &ClusterName, - role_name: &RoleName, - role_group_name: &RoleGroupName, -) -> String { - // Compile-time check - const _: () = assert!( - ClusterName::MAX_LENGTH - + 1 /* dash */ - + RoleName::MAX_LENGTH - + 1 /* dash */ - + RoleGroupName::MAX_LENGTH - + 1 /* dash */ - + 4 /* digits */ - <= OBJECT_NAME_MAX_LENGTH, - "The maximum lengths of the cluster name, role name and role group name must be defined so that the combination of these names (including separators and the sequential pod number) is also a valid object name with a maximum of 263 characters (see RFC 1123)" - ); - - format!( - "{}-{}-{}", - cluster_name.to_object_name(), - role_name.to_object_name(), - role_group_name.to_object_name() - ) -} - #[cfg(test)] mod tests { use std::str::FromStr; - use super::{ClusterName, RoleGroupName, RoleName, to_qualified_role_group_name}; use crate::framework::ProductName; #[test] @@ -217,18 +189,4 @@ mod tests { .is_err() ); } - - #[test] - fn test_qualified_role_group_name() { - let qualified_role_group_name = to_qualified_role_group_name( - &ClusterName::from_str("test-cluster").expect("should be a valid cluster name"), - &RoleName::from_str("data-nodes").expect("should be a valid role name"), - &RoleGroupName::from_str("ssd-storage").expect("should be a valid role group name"), - ); - - assert_eq!( - "test-cluster-data-nodes-ssd-storage", - qualified_role_group_name - ); - } } diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs index de27104..3316be4 100644 --- a/rust/operator-binary/src/framework/cluster_resources.rs +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -6,7 +6,7 @@ use stackable_operator::{ use super::{ ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, ProductName, }; -use crate::framework::kvp::label::LABEL_VALUE_MAX_LENGTH; +use crate::framework::kvp::label::MAX_LABEL_VALUE_LENGTH; pub fn cluster_resources_new( product_name: &ProductName, @@ -19,7 +19,7 @@ pub fn cluster_resources_new( // `-operator`. For the resulting label value to be valid, it must not exceed 63 characters. // Check at compile time that ProductName::MAX_LENGTH is defined accordingly. const _: () = assert!( - ProductName::MAX_LENGTH + "-operator".len() <= LABEL_VALUE_MAX_LENGTH, + ProductName::MAX_LENGTH + "-operator".len() <= MAX_LABEL_VALUE_LENGTH, "The label value `-operator` must not exceed 63 characters." ); diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs index ec96661..4c1de0e 100644 --- a/rust/operator-binary/src/framework/kvp/label.rs +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -8,7 +8,7 @@ use crate::framework::{ RoleName, }; -pub const LABEL_VALUE_MAX_LENGTH: usize = 63; +pub const MAX_LABEL_VALUE_LENGTH: usize = 63; /// Infallible variant of `Labels::recommended` pub fn recommended_labels( diff --git a/rust/operator-binary/src/framework/role_group_utils.rs b/rust/operator-binary/src/framework/role_group_utils.rs new file mode 100644 index 0000000..e4b522a --- /dev/null +++ b/rust/operator-binary/src/framework/role_group_utils.rs @@ -0,0 +1,92 @@ +use super::{ClusterName, RoleGroupName, RoleName}; +use crate::framework::{HasObjectName, MAX_OBJECT_NAME_LENGTH, kvp::label::MAX_LABEL_VALUE_LENGTH}; + +pub struct ResourceNames { + pub cluster_name: ClusterName, + pub role_name: RoleName, + pub role_group_name: RoleGroupName, +} + +impl ResourceNames { + // used at compile-time + #[allow(dead_code)] + const MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH: usize = ClusterName::MAX_LENGTH + + 1 // dash + + RoleName::MAX_LENGTH + + 1 // dash + + RoleGroupName::MAX_LENGTH; + + /// Creates a qualified role group name consisting of the cluster name, role name and role-group + /// name. + /// The result is a valid DNS subdomain name as defined in RFC 1123 that can be used e.g. as a name + /// for a StatefulSet. + fn qualified_role_group_name(&self) -> String { + format!( + "{}-{}-{}", + self.cluster_name.to_object_name(), + self.role_name.to_object_name(), + self.role_group_name.to_object_name() + ) + } + + pub fn role_group_config_map(&self) -> String { + // Compile-time check + const _: () = assert!( + ResourceNames::MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH <= MAX_OBJECT_NAME_LENGTH, + "The ConfigMap name `--` must not exceed 253 characters." + ); + + self.qualified_role_group_name() + } + + pub fn stateful_set_name(&self) -> String { + // Compile-time check + const _: () = assert!( + // see https://github.com/kubernetes/kubernetes/issues/64023 + ResourceNames::MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH + + 1 // dash + + 10 // digits for the controller-revision-hash label + <= MAX_LABEL_VALUE_LENGTH, + "The maximum lengths of the cluster name, role name and role group name must be defined so that the combination of these names (including separators and the sequential pod number or hash) is also a valid object name with a maximum of 63 characters (see RFC 1123)" + ); + + self.qualified_role_group_name() + } + + pub fn headless_service_name(&self) -> String { + const SUFFIX: &str = "-headless"; + + // Compile-time check + const _: () = assert!( + ResourceNames::MAX_QUALIFIED_ROLE_GROUP_NAME_LENGTH + SUFFIX.len() + <= MAX_LABEL_VALUE_LENGTH, + "The Service name `---headless` must not exceed 63 characters." + ); + + format!("{}{SUFFIX}", self.qualified_role_group_name()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::{ClusterName, RoleGroupName, RoleName}; + use crate::framework::role_group_utils::ResourceNames; + + #[test] + fn test_stateful_set_name() { + let resource_names = ResourceNames { + cluster_name: ClusterName::from_str("test-cluster") + .expect("should be a valid cluster name"), + role_name: RoleName::from_str("data-nodes").expect("should be a valid role name"), + role_group_name: RoleGroupName::from_str("ssd-storage") + .expect("should be a valid role group name"), + }; + + assert_eq!( + "test-cluster-data-nodes-ssd-storage", + resource_names.stateful_set_name() + ); + } +} diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs index cba93ef..6e3f214 100644 --- a/rust/operator-binary/src/framework/role_utils.rs +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -11,6 +11,9 @@ use stackable_operator::{ schemars::JsonSchema, }; +use super::ProductName; +use crate::framework::{ClusterName, MAX_OBJECT_NAME_LENGTH, kvp::label::MAX_LABEL_VALUE_LENGTH}; + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct GenericProductSpecificCommonConfig {} @@ -170,3 +173,48 @@ where merged_config.merge(&role_config); merged_config } + +pub struct ResourceNames { + pub cluster_name: ClusterName, + pub product_name: ProductName, +} + +impl ResourceNames { + pub fn service_account_name(&self) -> String { + const SUFFIX: &str = "-serviceaccount"; + + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH + SUFFIX.len() <= MAX_OBJECT_NAME_LENGTH, + "The ServiceAccount name `-serviceaccount` must not exceed 253 characters." + ); + + format!("{}{SUFFIX}", self.cluster_name) + } + + pub fn role_binding_name(&self) -> String { + const SUFFIX: &str = "-rolebinding"; + + // No compile-time check, because RoleBinding names do not seem to be restricted. + + format!("{}{SUFFIX}", self.cluster_name) + } + + pub fn cluster_role_name(&self) -> String { + const SUFFIX: &str = "-clusterrole"; + + // No compile-time check, because ClusterRole names do not seem to be restricted. + + format!("{}{SUFFIX}", self.product_name) + } + + pub fn discovery_service_name(&self) -> String { + // Compile-time check + const _: () = assert!( + ClusterName::MAX_LENGTH <= MAX_LABEL_VALUE_LENGTH, + "The Service name `` must not exceed 63 characters." + ); + + format!("{}", self.cluster_name) + } +} diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index 2297ba1..edcffd7 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -32,7 +32,7 @@ spec: app.kubernetes.io/instance: opensearch app.kubernetes.io/name: opensearch app.kubernetes.io/role-group: cluster-manager - serviceName: "" + serviceName: opensearch-nodes-cluster-manager-headless template: metadata: labels: @@ -51,12 +51,10 @@ spec: env: - name: DISABLE_INSTALL_DEMO_CONFIG value: "true" - # - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD - # value: super@Secret1 - name: cluster.initial_cluster_manager_nodes value: opensearch-nodes-cluster-manager-0,opensearch-nodes-cluster-manager-1,opensearch-nodes-cluster-manager-2 - name: discovery.seed_hosts - value: opensearch-cluster-manager + value: opensearch - name: node.name valueFrom: fieldRef: @@ -74,9 +72,6 @@ spec: - containerPort: 9300 name: transport protocol: TCP - - containerPort: 9600 - name: metrics - protocol: TCP readinessProbe: failureThreshold: 3 periodSeconds: 5 @@ -114,6 +109,8 @@ spec: readOnly: true securityContext: fsGroup: 1000 + serviceAccount: opensearch-serviceaccount + serviceAccountName: opensearch-serviceaccount terminationGracePeriodSeconds: 30 volumes: - configMap: @@ -184,7 +181,7 @@ spec: app.kubernetes.io/instance: opensearch app.kubernetes.io/name: opensearch app.kubernetes.io/role-group: data - serviceName: "" + serviceName: opensearch-nodes-data-headless template: metadata: labels: @@ -205,11 +202,9 @@ spec: env: - name: DISABLE_INSTALL_DEMO_CONFIG value: "true" - # - name: OPENSEARCH_INITIAL_ADMIN_PASSWORD - # value: super@Secret1 - name: cluster.initial_cluster_manager_nodes - name: discovery.seed_hosts - value: opensearch-cluster-manager + value: opensearch - name: node.name valueFrom: fieldRef: @@ -227,9 +222,6 @@ spec: - containerPort: 9300 name: transport protocol: TCP - - containerPort: 9600 - name: metrics - protocol: TCP readinessProbe: failureThreshold: 3 periodSeconds: 5 @@ -267,6 +259,8 @@ spec: readOnly: true securityContext: fsGroup: 1000 + serviceAccount: opensearch-serviceaccount + serviceAccountName: opensearch-serviceaccount terminationGracePeriodSeconds: 30 volumes: - configMap: @@ -375,9 +369,10 @@ metadata: app.kubernetes.io/instance: opensearch app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager app.kubernetes.io/version: 3.0.0 stackable.tech/vendor: Stackable - name: opensearch-cluster-manager + name: opensearch-nodes-cluster-manager-headless spec: ports: - name: http @@ -388,13 +383,12 @@ spec: port: 9300 protocol: TCP targetPort: 9300 - - name: metrics - port: 9600 - protocol: TCP - targetPort: 9600 publishNotReadyAddresses: true selector: - cluster_manager: "true" + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + app.kubernetes.io/role-group: cluster-manager type: ClusterIP --- apiVersion: v1 @@ -405,10 +399,10 @@ metadata: app.kubernetes.io/instance: opensearch app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster app.kubernetes.io/name: opensearch - app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/role-group: data app.kubernetes.io/version: 3.0.0 stackable.tech/vendor: Stackable - name: opensearch-nodes-cluster-manager + name: opensearch-nodes-data-headless spec: ports: - name: http @@ -419,16 +413,12 @@ spec: port: 9300 protocol: TCP targetPort: 9300 - - name: metrics - port: 9600 - protocol: TCP - targetPort: 9600 publishNotReadyAddresses: true selector: app.kubernetes.io/component: nodes app.kubernetes.io/instance: opensearch app.kubernetes.io/name: opensearch - app.kubernetes.io/role-group: cluster-manager + app.kubernetes.io/role-group: data type: ClusterIP --- apiVersion: v1 @@ -439,10 +429,9 @@ metadata: app.kubernetes.io/instance: opensearch app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster app.kubernetes.io/name: opensearch - app.kubernetes.io/role-group: data app.kubernetes.io/version: 3.0.0 stackable.tech/vendor: Stackable - name: opensearch-nodes-data + name: opensearch spec: ports: - name: http @@ -453,17 +442,51 @@ spec: port: 9300 protocol: TCP targetPort: 9300 - - name: metrics - port: 9600 - protocol: TCP - targetPort: 9600 publishNotReadyAddresses: true selector: + cluster_manager: "true" + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: app.kubernetes.io/component: nodes app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster app.kubernetes.io/name: opensearch - app.kubernetes.io/role-group: data - type: ClusterIP + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-serviceaccount + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/managed-by: opensearch.stackable.tech_opensearchcluster + app.kubernetes.io/name: opensearch + app.kubernetes.io/version: 3.0.0 + stackable.tech/vendor: Stackable + name: opensearch-rolebinding + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: opensearch-clusterrole +subjects: +- kind: ServiceAccount + name: opensearch-serviceaccount --- apiVersion: policy/v1 kind: PodDisruptionBudget diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 8b80df7..9d68547 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -1,4 +1,3 @@ -# TODO Test with OpenShift --- apiVersion: opensearch.stackable.tech/v1alpha1 kind: OpenSearchCluster @@ -51,7 +50,6 @@ spec: envOverrides: # TODO Make these the defaults in the image DISABLE_INSTALL_DEMO_CONFIG: "true" - # OPENSEARCH_INITIAL_ADMIN_PASSWORD: super@Secret1 configOverrides: # TODO Add the required options to the operator opensearch.yml: diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml index d5a086b..cb6b515 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml @@ -74,7 +74,7 @@ data: # TODO Use a discovery ConfigMap - host = 'opensearch-cluster-manager' + host = 'opensearch' port = 9200 auth = ('admin', 'AJVFsGJBbpT6mChn') # For testing only. Don't store credentials in code. ca_certs_path = '/stackable/tls/ca.crt' From 320937582a117fcf66c8925814511bb43d47777d Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 14 Jul 2025 09:47:45 +0200 Subject: [PATCH 25/35] Implement terminationGracePeriodSeconds --- rust/operator-binary/src/controller.rs | 16 +++++- rust/operator-binary/src/controller/apply.rs | 9 ++- rust/operator-binary/src/controller/build.rs | 6 +- .../controller/build/role_group_builder.rs | 56 ++++++++++--------- .../src/controller/update_status.rs | 4 +- .../src/controller/validate.rs | 48 ++++++++++++++-- rust/operator-binary/src/crd/mod.rs | 29 +++++++--- .../src/framework/role_utils.rs | 17 ------ tests/templates/kuttl/smoke/10-assert.yaml | 4 +- .../kuttl/smoke/10-install-opensearch.yaml | 1 + 10 files changed, 120 insertions(+), 70 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index f563c90..64ea4b9 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -22,7 +22,10 @@ use update_status::update_status; use validate::validate; use crate::{ - crd::v1alpha1::{self}, + crd::{ + NodeRoles, + v1alpha1::{self}, + }, framework::{ ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, OperatorName, ProductName, ProductVersion, RoleGroupName, @@ -96,7 +99,14 @@ impl ReconcilerError for Error { } type OpenSearchRoleGroupConfig = - RoleGroupConfig; + RoleGroupConfig; + +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedOpenSearchConfig { + pub node_roles: NodeRoles, + pub resources: stackable_operator::commons::resources::Resources, + pub termination_grace_period_seconds: i64, +} // validated and converted to validated and safe types // no user errors @@ -255,7 +265,7 @@ pub async fn reconcile( struct Prepared; struct Applied; -struct Resources { +struct KubernetesResources { stateful_sets: Vec, services: Vec, config_maps: Vec, diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 57985f5..23b6f2e 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -7,7 +7,7 @@ use stackable_operator::{ }; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{Applied, ContextNames, Prepared, Resources}; +use super::{Applied, ContextNames, KubernetesResources, Prepared}; use crate::framework::{ HasNamespace, HasObjectName, HasUid, cluster_resources::cluster_resources_new, }; @@ -54,7 +54,10 @@ impl<'a> Applier<'a> { } } - pub async fn apply(mut self, resources: Resources) -> Result> { + pub async fn apply( + mut self, + resources: KubernetesResources, + ) -> Result> { let stateful_sets = self.add_resources(resources.stateful_sets).await?; let services = self.add_resources(resources.services).await?; @@ -72,7 +75,7 @@ impl<'a> Applier<'a> { .await .context(DeleteOrphanedResourcesSnafu)?; - Ok(Resources { + Ok(KubernetesResources { stateful_sets, services, config_maps, diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 5b88026..367c0df 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -2,14 +2,14 @@ use std::{marker::PhantomData, str::FromStr}; use role_builder::RoleBuilder; -use super::{ContextNames, Prepared, Resources, ValidatedCluster}; +use super::{ContextNames, KubernetesResources, Prepared, ValidatedCluster}; use crate::framework::RoleName; pub mod node_config; pub mod role_builder; pub mod role_group_builder; -pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> Resources { +pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResources { let mut config_maps = vec![]; let mut stateful_sets = vec![]; let mut services = vec![]; @@ -33,7 +33,7 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> Resources RoleGroupBuilder<'a> { } fn build_pod_template(&self) -> PodTemplateSpec { - let mut builder = PodBuilder::new(); - let mut node_role_labels = Labels::new(); for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels.insert( @@ -153,24 +148,35 @@ impl<'a> RoleGroupBuilder<'a> { let container = self.build_container(&self.role_group_config); - let mut pod_template = builder - .metadata(metadata) - .add_container(container) - .add_volume(Volume { - name: CONFIG_VOLUME_NAME.to_owned(), - config_map: Some(ConfigMapVolumeSource { - name: self.resource_names.role_group_config_map(), - ..Default::default() + // The PodBuilder is not used because it re-validates the values which are already + // validated. For instance, it would be necessary to convert the + // termination_grace_period_seconds into a Duration, the PodBuilder parses the Duration, + // converts it back into seconds and fails if this is not possible. + let mut pod_template = PodTemplateSpec { + metadata: Some(metadata), + spec: Some(PodSpec { + containers: vec![container], + security_context: Some(PodSecurityContext { + fs_group: Some(1000), + ..PodSecurityContext::default() }), - ..Default::default() - }) - .expect("The volume names are statically defined and there should be no duplicates.") - .security_context(PodSecurityContext { - fs_group: Some(1000), - ..PodSecurityContext::default() - }) - .service_account_name(&self.service_account_name) - .build_template(); + service_account_name: Some(self.service_account_name.clone()), + termination_grace_period_seconds: Some( + self.role_group_config + .config + .termination_grace_period_seconds, + ), + volumes: Some(vec![Volume { + name: CONFIG_VOLUME_NAME.to_owned(), + config_map: Some(ConfigMapVolumeSource { + name: self.resource_names.role_group_config_map(), + ..Default::default() + }), + ..Volume::default() + }]), + ..PodSpec::default() + }), + }; pod_template.merge_from(self.role_group_config.pod_overrides.clone()); diff --git a/rust/operator-binary/src/controller/update_status.rs b/rust/operator-binary/src/controller/update_status.rs index 85a4aeb..923f7db 100644 --- a/rust/operator-binary/src/controller/update_status.rs +++ b/rust/operator-binary/src/controller/update_status.rs @@ -8,7 +8,7 @@ use stackable_operator::{ }; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{Applied, ContextNames, Resources}; +use super::{Applied, ContextNames, KubernetesResources}; use crate::crd::v1alpha1::{self, OpenSearchClusterStatus}; #[derive(Snafu, Debug, EnumDiscriminants)] @@ -36,7 +36,7 @@ pub async fn update_status( client: &Client, names: &ContextNames, cluster: &v1alpha1::OpenSearchCluster, - applied_resources: Resources, + applied_resources: KubernetesResources, ) -> Result<()> { let mut stateful_set_condition_builder = StatefulSetConditionBuilder::default(); for stateful_set in applied_resources.stateful_sets { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 0cb83f3..5ec5bff 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -1,12 +1,16 @@ -use std::{collections::BTreeMap, str::FromStr}; +use std::{collections::BTreeMap, num::TryFromIntError, str::FromStr}; use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_operator::kube::{Resource, ResourceExt}; +use stackable_operator::{ + kube::{Resource, ResourceExt}, + role_utils::RoleGroup, + time::Duration, +}; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{ProductVersion, RoleGroupName, ValidatedCluster}; +use super::{ProductVersion, RoleGroupName, ValidatedCluster, ValidatedOpenSearchConfig}; use crate::{ - crd::v1alpha1, + crd::v1alpha1::{self, OpenSearchConfig}, framework::{ ClusterName, role_utils::{RoleGroupConfig, with_validated_config}, @@ -38,10 +42,17 @@ pub enum Error { ValidateOpenSearchConfig { source: stackable_operator::config::fragment::ValidationError, }, + + #[snafu(display("termination grace period is too long (got {duration}, maximum allowed is {max})", max = Duration::from_secs(i64::MAX as u64)))] + TerminationGracePeriodTooLong { + source: TryFromIntError, + duration: Duration, + }, } type Result = std::result::Result; +// TODO split // no client needed pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result { let raw_cluster_name = cluster.meta().name.clone().context(GetClusterNameSnafu)?; @@ -59,13 +70,38 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result = with_validated_config( role_group_config, &cluster.spec.nodes, &v1alpha1::OpenSearchConfig::default_config(), ) .context(ValidateOpenSearchConfigSnafu)?; - let validated_role_group_config = RoleGroupConfig::from(validated_role_group); + + let graceful_shutdown_timeout = merged_role_group.config.config.graceful_shutdown_timeout; + + let termination_grace_period_seconds = graceful_shutdown_timeout + .as_secs() + .try_into() + .context(TerminationGracePeriodTooLongSnafu { + duration: graceful_shutdown_timeout, + })?; + + let validated_config = ValidatedOpenSearchConfig { + node_roles: merged_role_group.config.config.node_roles, + resources: merged_role_group.config.config.resources, + termination_grace_period_seconds, + }; + + let validated_role_group_config = RoleGroupConfig { + // Kubernetes defaults to 1 if not set + replicas: merged_role_group.replicas.unwrap_or(1), + config: validated_config, + config_overrides: merged_role_group.config.config_overrides, + env_overrides: merged_role_group.config.env_overrides, + cli_overrides: merged_role_group.config.cli_overrides, + pod_overrides: merged_role_group.config.pod_overrides, + product_specific_common_config: merged_role_group.config.product_specific_common_config, + }; role_group_configs.insert(role_group_name, validated_role_group_config); } diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 6ba2081..2ca82c2 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,4 +1,4 @@ -use std::slice; +use std::{slice, str::FromStr}; use serde::{Deserialize, Serialize}; use stackable_operator::{ @@ -19,6 +19,7 @@ use stackable_operator::{ role_utils::{GenericRoleConfig, Role}, schemars::{self, JsonSchema}, status::condition::{ClusterCondition, HasStatusCondition}, + time::Duration, versioned::versioned, }; use strum::Display; @@ -115,6 +116,11 @@ pub mod versioned { #[fragment_attrs(serde(default))] pub resources: Resources, + + /// Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the + /// operator documentation for details. + #[fragment_attrs(serde(default))] + pub graceful_shutdown_timeout: Duration, } #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] @@ -159,6 +165,14 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { impl v1alpha1::OpenSearchConfig { pub fn default_config() -> v1alpha1::OpenSearchConfigFragment { v1alpha1::OpenSearchConfigFragment { + // Defaults taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L16-L20 + node_roles: Some(NodeRoles(vec![ + v1alpha1::NodeRole::ClusterManager, + v1alpha1::NodeRole::Ingest, + v1alpha1::NodeRole::Data, + v1alpha1::NodeRole::RemoteClusterClient, + ])), resources: ResourcesFragment { memory: MemoryLimitsFragment { // An idle node already requires 2 Gi. @@ -184,14 +198,11 @@ impl v1alpha1::OpenSearchConfig { }, }, }, - // Defaults taken from the Helm chart, see - // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L16-L20 - node_roles: Some(NodeRoles(vec![ - v1alpha1::NodeRole::ClusterManager, - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient, - ])), + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L364 + graceful_shutdown_timeout: Some( + Duration::from_str("2m").expect("should be a valid duration"), + ), } } } diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs index 6e3f214..db0c5d7 100644 --- a/rust/operator-binary/src/framework/role_utils.rs +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -45,23 +45,6 @@ impl RoleGroupConfig From> - for RoleGroupConfig -{ - fn from(value: RoleGroup) -> Self { - RoleGroupConfig { - // Kubernetes defaults to 1 if not set - replicas: value.replicas.unwrap_or(1), - config: value.config.config, - config_overrides: value.config.config_overrides, - env_overrides: value.config.env_overrides, - cli_overrides: value.config.cli_overrides, - pod_overrides: value.config.pod_overrides, - product_specific_common_config: value.config.product_specific_common_config, - } - } -} - // RoleGroup::validate_config with fixed types pub fn validate_config( role_group: &RoleGroup, diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index edcffd7..8cb0b1d 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -111,7 +111,7 @@ spec: fsGroup: 1000 serviceAccount: opensearch-serviceaccount serviceAccountName: opensearch-serviceaccount - terminationGracePeriodSeconds: 30 + terminationGracePeriodSeconds: 180 volumes: - configMap: defaultMode: 420 @@ -261,7 +261,7 @@ spec: fsGroup: 1000 serviceAccount: opensearch-serviceaccount serviceAccountName: opensearch-serviceaccount - terminationGracePeriodSeconds: 30 + terminationGracePeriodSeconds: 120 volumes: - configMap: defaultMode: 420 diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index 9d68547..c128b04 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -17,6 +17,7 @@ spec: storage: data: capacity: 100Mi + gracefulShutdownTimeout: 3m replicas: 3 podOverrides: spec: From 239a379114bfa4d2c4b898ecdc34a8fc4d8bb012 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 14 Jul 2025 14:31:39 +0200 Subject: [PATCH 26/35] Implement affinities --- rust/operator-binary/src/controller.rs | 11 +++-- rust/operator-binary/src/controller/build.rs | 7 +-- .../src/controller/build/node_config.rs | 19 +++---- .../src/controller/build/role_builder.rs | 17 ++----- .../controller/build/role_group_builder.rs | 36 +++++++++----- .../src/controller/validate.rs | 16 ++++-- rust/operator-binary/src/crd/mod.rs | 49 ++++++++++++++----- tests/templates/kuttl/smoke/10-assert.yaml | 25 +++++++++- .../kuttl/smoke/10-install-opensearch.yaml | 1 - 9 files changed, 125 insertions(+), 56 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 64ea4b9..1c8c7b7 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -5,7 +5,7 @@ use build::build; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, - commons::product_image_selection::ProductImage, + commons::{affinity::StackableAffinity, product_image_selection::ProductImage}, k8s_openapi::api::{ apps::v1::StatefulSet, core::v1::{ConfigMap, Service, ServiceAccount}, @@ -28,7 +28,7 @@ use crate::{ }, framework::{ ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, - OperatorName, ProductName, ProductVersion, RoleGroupName, + OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, }, }; @@ -103,6 +103,7 @@ type OpenSearchRoleGroupConfig = #[derive(Clone, Debug, PartialEq)] pub struct ValidatedOpenSearchConfig { + pub affinity: StackableAffinity, pub node_roles: NodeRoles, pub resources: stackable_operator::commons::resources::Resources, pub termination_grace_period_seconds: i64, @@ -125,6 +126,10 @@ pub struct ValidatedCluster { } impl ValidatedCluster { + pub fn role_name() -> RoleName { + RoleName::from_str("nodes").expect("should be a valid role name") + } + pub fn is_single_node(&self) -> bool { self.node_count() == 1 } @@ -233,7 +238,7 @@ pub async fn reconcile( // dereference (client required) // validate (no client required) - let validated_cluster = validate(cluster).context(ValidateClusterSnafu)?; + let validated_cluster = validate(&context.names, cluster).context(ValidateClusterSnafu)?; // build (no client required; infallible) let prepared_resources = build(&context.names, validated_cluster.clone()); diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 367c0df..28aa9ad 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -1,9 +1,8 @@ -use std::{marker::PhantomData, str::FromStr}; +use std::marker::PhantomData; use role_builder::RoleBuilder; use super::{ContextNames, KubernetesResources, Prepared, ValidatedCluster}; -use crate::framework::RoleName; pub mod node_config; pub mod role_builder; @@ -14,9 +13,7 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou let mut stateful_sets = vec![]; let mut services = vec![]; - let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); - - let role_builder = RoleBuilder::new(role_name, cluster.clone(), names); + let role_builder = RoleBuilder::new(cluster.clone(), names); for role_group_builder in role_builder.role_group_builders() { config_maps.push(role_group_builder.build_config_map()); diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 09ba5a8..794d3cb 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -5,7 +5,7 @@ use super::ValidatedCluster; use crate::{ controller::OpenSearchRoleGroupConfig, crd::v1alpha1, - framework::{RoleName, builder::pod::container::EnvVarSet, role_group_utils}, + framework::{builder::pod::container::EnvVarSet, role_group_utils}, }; pub const CONFIGURATION_FILE_OPENSEARCH_YML: &str = "opensearch.yml"; @@ -67,7 +67,6 @@ pub const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; pub const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; pub struct NodeConfig { - role_name: RoleName, cluster: ValidatedCluster, role_group_config: OpenSearchRoleGroupConfig, discovery_service_name: String, @@ -77,13 +76,11 @@ pub struct NodeConfig { // variables. impl NodeConfig { pub fn new( - role_name: RoleName, cluster: ValidatedCluster, role_group_config: OpenSearchRoleGroupConfig, discovery_service_name: String, ) -> Self { Self { - role_name, cluster, role_group_config, discovery_service_name, @@ -221,7 +218,7 @@ impl NodeConfig { for (role_group_name, role_group_config) in cluster_manager_configs { let role_group_resource_names = role_group_utils::ResourceNames { cluster_name: self.cluster.name.clone(), - role_name: self.role_name.clone(), + role_name: ValidatedCluster::role_name(), role_group_name, }; @@ -249,7 +246,10 @@ mod tests { }; use stackable_operator::{ - commons::{product_image_selection::ProductImage, resources::Resources}, + commons::{ + affinity::StackableAffinity, product_image_selection::ProductImage, + resources::Resources, + }, k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector, PodTemplateSpec}, kube::api::ObjectMeta, role_utils::GenericRoleConfig, @@ -257,6 +257,7 @@ mod tests { use super::*; use crate::{ + controller::ValidatedOpenSearchConfig, crd::NodeRoles, framework::{ClusterName, ProductVersion, role_utils::GenericProductSpecificCommonConfig}, }; @@ -277,13 +278,14 @@ mod tests { role_config: GenericRoleConfig::default(), role_group_configs: BTreeMap::new(), }; - let role_name = RoleName::from_str("nodes").expect("should be a valid role name"); let role_group_config = OpenSearchRoleGroupConfig { replicas: 1, - config: v1alpha1::OpenSearchConfig { + config: ValidatedOpenSearchConfig { + affinity: StackableAffinity::default(), node_roles: NodeRoles::default(), resources: Resources::default(), + termination_grace_period_seconds: 30, }, config_overrides: HashMap::default(), env_overrides: [("TEST".to_owned(), "value".to_owned())].into(), @@ -293,7 +295,6 @@ mod tests { }; let node_config = NodeConfig::new( - role_name, cluster, role_group_config, "my-opensearch-cluster-manager".to_owned(), diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index a294086..213c533 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -22,7 +22,7 @@ use crate::{ controller::{ContextNames, ValidatedCluster}, crd::v1alpha1, framework::{ - RoleName, + IsLabelValue, builder::{ meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, }, @@ -33,20 +33,14 @@ use crate::{ const PDB_DEFAULT_MAX_UNAVAILABLE: u16 = 1; pub struct RoleBuilder<'a> { - role_name: RoleName, cluster: ValidatedCluster, context_names: &'a ContextNames, resource_names: ResourceNames, } impl<'a> RoleBuilder<'a> { - pub fn new( - role_name: RoleName, - cluster: ValidatedCluster, - context_names: &'a ContextNames, - ) -> RoleBuilder<'a> { + pub fn new(cluster: ValidatedCluster, context_names: &'a ContextNames) -> RoleBuilder<'a> { RoleBuilder { - role_name: role_name.clone(), cluster: cluster.clone(), context_names, resource_names: ResourceNames { @@ -64,7 +58,6 @@ impl<'a> RoleBuilder<'a> { .iter() .map(|(role_group_name, role_group_config)| { RoleGroupBuilder::new( - self.role_name.clone(), self.resource_names.service_account_name(), self.cluster.clone(), role_group_name.clone(), @@ -154,7 +147,7 @@ impl<'a> RoleBuilder<'a> { pod_disruption_budget_builder_with_role( &self.cluster, &self.context_names.product_name, - &self.role_name, + &ValidatedCluster::role_name(), &self.context_names.operator_name, &self.context_names.controller_name, ) @@ -184,8 +177,8 @@ impl<'a> RoleBuilder<'a> { // Well-known Kubernetes labels let mut labels = Labels::role_selector( &self.cluster, - &self.context_names.product_name.to_string(), - &self.role_name.to_string(), + &self.context_names.product_name.to_label_value(), + &ValidatedCluster::role_name().to_label_value(), ) .unwrap(); diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 2f2e23a..24b364e 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -5,9 +5,9 @@ use stackable_operator::{ api::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ - ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, PodSecurityContext, - PodSpec, PodTemplateSpec, Probe, Service, ServicePort, ServiceSpec, - TCPSocketAction, Volume, VolumeMount, + Affinity, ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, + PodSecurityContext, PodSpec, PodTemplateSpec, Probe, Service, ServicePort, + ServiceSpec, TCPSocketAction, Volume, VolumeMount, }, }, apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, @@ -20,7 +20,7 @@ use super::node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}; use crate::{ controller::{ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster}, framework::{ - RoleGroupName, RoleName, + RoleGroupName, builder::meta::ownerreference_from_resource, kvp::label::{recommended_labels, role_group_selector}, role_group_utils::ResourceNames, @@ -39,7 +39,6 @@ const DATA_VOLUME_NAME: &str = "data"; const OPENSEARCH_BASE_PATH: &str = "/usr/share/opensearch"; pub struct RoleGroupBuilder<'a> { - role_name: RoleName, service_account_name: String, cluster: ValidatedCluster, node_config: NodeConfig, @@ -51,7 +50,6 @@ pub struct RoleGroupBuilder<'a> { impl<'a> RoleGroupBuilder<'a> { pub fn new( - role_name: RoleName, service_account_name: String, cluster: ValidatedCluster, role_group_name: RoleGroupName, @@ -60,11 +58,9 @@ impl<'a> RoleGroupBuilder<'a> { discovery_service_name: String, ) -> RoleGroupBuilder<'a> { RoleGroupBuilder { - role_name: role_name.clone(), service_account_name, cluster: cluster.clone(), node_config: NodeConfig::new( - role_name.clone(), cluster.clone(), role_group_config.clone(), discovery_service_name, @@ -74,7 +70,7 @@ impl<'a> RoleGroupBuilder<'a> { context_names, resource_names: ResourceNames { cluster_name: cluster.name.clone(), - role_name, + role_name: ValidatedCluster::role_name(), role_group_name, }, } @@ -136,6 +132,7 @@ impl<'a> RoleGroupBuilder<'a> { let mut node_role_labels = Labels::new(); for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels.insert( + // TODO Prefix the key Label::try_from((format!("{node_role}"), "true".to_string())) .expect("should be a valid label"), ); @@ -155,7 +152,24 @@ impl<'a> RoleGroupBuilder<'a> { let mut pod_template = PodTemplateSpec { metadata: Some(metadata), spec: Some(PodSpec { + affinity: Some(Affinity { + node_affinity: self.role_group_config.config.affinity.node_affinity.clone(), + pod_affinity: self.role_group_config.config.affinity.pod_affinity.clone(), + pod_anti_affinity: self + .role_group_config + .config + .affinity + .pod_anti_affinity + .clone(), + }), containers: vec![container], + node_selector: self + .role_group_config + .config + .affinity + .node_selector + .clone() + .map(|wrapped| wrapped.node_selector), security_context: Some(PodSecurityContext { fs_group: Some(1000), ..PodSecurityContext::default() @@ -326,7 +340,7 @@ impl<'a> RoleGroupBuilder<'a> { &self.cluster.product_version, &self.context_names.operator_name, &self.context_names.controller_name, - &self.role_name, + &ValidatedCluster::role_name(), &self.role_group_name, ) } @@ -335,7 +349,7 @@ impl<'a> RoleGroupBuilder<'a> { role_group_selector( &self.cluster, &self.context_names.product_name, - &self.role_name, + &ValidatedCluster::role_name(), &self.role_group_name, ) } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 5ec5bff..569fb21 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -8,7 +8,9 @@ use stackable_operator::{ }; use strum::{EnumDiscriminants, IntoStaticStr}; -use super::{ProductVersion, RoleGroupName, ValidatedCluster, ValidatedOpenSearchConfig}; +use super::{ + ContextNames, ProductVersion, RoleGroupName, ValidatedCluster, ValidatedOpenSearchConfig, +}; use crate::{ crd::v1alpha1::{self, OpenSearchConfig}, framework::{ @@ -54,7 +56,10 @@ type Result = std::result::Result; // TODO split // no client needed -pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result { +pub fn validate( + names: &ContextNames, + cluster: &v1alpha1::OpenSearchCluster, +) -> Result { let raw_cluster_name = cluster.meta().name.clone().context(GetClusterNameSnafu)?; let cluster_name = ClusterName::from_str(&raw_cluster_name).context(ParseClusterNameSnafu)?; @@ -73,7 +78,11 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result = with_validated_config( role_group_config, &cluster.spec.nodes, - &v1alpha1::OpenSearchConfig::default_config(), + &v1alpha1::OpenSearchConfig::default_config( + &names.product_name, + &cluster_name, + &ValidatedCluster::role_name(), + ), ) .context(ValidateOpenSearchConfigSnafu)?; @@ -87,6 +96,7 @@ pub fn validate(cluster: &v1alpha1::OpenSearchCluster) -> Result, + pub affinity: StackableAffinity, /// Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the /// operator documentation for details. #[fragment_attrs(serde(default))] pub graceful_shutdown_timeout: Duration, + + pub node_roles: NodeRoles, + + #[fragment_attrs(serde(default))] + pub resources: Resources, } #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] @@ -163,8 +170,33 @@ impl HasStatusCondition for v1alpha1::OpenSearchCluster { } impl v1alpha1::OpenSearchConfig { - pub fn default_config() -> v1alpha1::OpenSearchConfigFragment { + pub fn default_config( + product_name: &ProductName, + cluster_name: &ClusterName, + role_name: &RoleName, + ) -> v1alpha1::OpenSearchConfigFragment { v1alpha1::OpenSearchConfigFragment { + affinity: StackableAffinityFragment { + pod_affinity: None, + pod_anti_affinity: Some(PodAntiAffinity { + preferred_during_scheduling_ignored_during_execution: Some(vec![ + affinity_between_role_pods( + &product_name.to_label_value(), + &cluster_name.to_label_value(), + &role_name.to_label_value(), + 1, + ), + ]), + required_during_scheduling_ignored_during_execution: None, + }), + node_affinity: None, + node_selector: None, + }, + // Default taken from the Helm chart, see + // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L364 + graceful_shutdown_timeout: Some( + Duration::from_str("2m").expect("should be a valid duration"), + ), // Defaults taken from the Helm chart, see // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L16-L20 node_roles: Some(NodeRoles(vec![ @@ -198,11 +230,6 @@ impl v1alpha1::OpenSearchConfig { }, }, }, - // Default taken from the Helm chart, see - // https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/values.yaml#L364 - graceful_shutdown_timeout: Some( - Duration::from_str("2m").expect("should be a valid duration"), - ), } } } diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index 8cb0b1d..baf3276 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -1,6 +1,7 @@ # All fields are checked that are set by the operator. # This helps to detect unintentional changes. # The maintenance effort should be okay as long as it is only done in the smoke test. +# TODO Check individual field in unit tests --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert @@ -45,6 +46,17 @@ spec: cluster_manager: "true" stackable.tech/vendor: Stackable spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + topologyKey: kubernetes.io/hostname + weight: 1 containers: - command: - /usr/share/opensearch/opensearch-docker-entrypoint.sh @@ -111,7 +123,7 @@ spec: fsGroup: 1000 serviceAccount: opensearch-serviceaccount serviceAccountName: opensearch-serviceaccount - terminationGracePeriodSeconds: 180 + terminationGracePeriodSeconds: 120 volumes: - configMap: defaultMode: 420 @@ -196,6 +208,17 @@ spec: remote_cluster_client: "true" stackable.tech/vendor: Stackable spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + topologyKey: kubernetes.io/hostname + weight: 1 containers: - command: - /usr/share/opensearch/opensearch-docker-entrypoint.sh diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml b/tests/templates/kuttl/smoke/10-install-opensearch.yaml index c128b04..9d68547 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml @@ -17,7 +17,6 @@ spec: storage: data: capacity: 100Mi - gracefulShutdownTimeout: 3m replicas: 3 podOverrides: spec: From 756df9ca06d994568fae629c827148306664c5b0 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 09:31:51 +0200 Subject: [PATCH 27/35] Add additional labels to the cluster manager service --- rust/operator-binary/src/controller.rs | 3 +- .../src/controller/build/role_builder.rs | 10 +- .../controller/build/role_group_builder.rs | 36 ++++++-- .../src/controller/validate.rs | 92 ++++++++++--------- .../src/framework/kvp/label.rs | 14 +++ tests/templates/kuttl/smoke/10-assert.yaml | 18 +++- 6 files changed, 110 insertions(+), 63 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 1c8c7b7..65da68f 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -255,8 +255,7 @@ pub async fn reconcile( .await .context(ApplyResourcesSnafu)?; - // create discovery ConfigMap - // TODO Think about: Address from Listener has to be added to some ConfigMap + // create discovery ConfigMap based on the applied resources (client required) // update status (client required) update_status(&context.client, &context.names, cluster, applied_resources) diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 213c533..6f3b44d 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -20,7 +20,6 @@ use super::role_group_builder::{ }; use crate::{ controller::{ContextNames, ValidatedCluster}, - crd::v1alpha1, framework::{ IsLabelValue, builder::{ @@ -113,18 +112,15 @@ impl<'a> RoleBuilder<'a> { let metadata = self.common_metadata(self.resource_names.discovery_service_name()); - let service_selector = [( - v1alpha1::NodeRole::ClusterManager.to_string(), - "true".to_owned(), - )] - .into(); + let service_selector = + RoleGroupBuilder::cluster_manager_labels(&self.cluster, self.context_names); let service_spec = ServiceSpec { // Internal communication does not need to be exposed type_: Some("ClusterIP".to_string()), cluster_ip: Some("None".to_string()), ports: Some(ports), - selector: Some(service_selector), + selector: Some(service_selector.into()), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() }; diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 24b364e..6ca788a 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -19,10 +19,11 @@ use stackable_operator::{ use super::node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}; use crate::{ controller::{ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster}, + crd::v1alpha1, framework::{ RoleGroupName, builder::meta::ownerreference_from_resource, - kvp::label::{recommended_labels, role_group_selector}, + kvp::label::{recommended_labels, role_group_selector, role_selector}, role_group_utils::ResourceNames, }, }; @@ -104,7 +105,6 @@ impl<'a> RoleGroupBuilder<'a> { .resources .storage .data - // TODO Compare name with Helm chart .build_pvc(DATA_VOLUME_NAME, Some(vec!["ReadWriteOnce"])); let spec = StatefulSetSpec { @@ -131,11 +131,7 @@ impl<'a> RoleGroupBuilder<'a> { fn build_pod_template(&self) -> PodTemplateSpec { let mut node_role_labels = Labels::new(); for node_role in self.role_group_config.config.node_roles.iter() { - node_role_labels.insert( - // TODO Prefix the key - Label::try_from((format!("{node_role}"), "true".to_string())) - .expect("should be a valid label"), - ); + node_role_labels.insert(Self::build_node_role_label(node_role)); } let metadata = ObjectMetaBuilder::new() @@ -197,6 +193,32 @@ impl<'a> RoleGroupBuilder<'a> { pod_template } + pub fn cluster_manager_labels( + cluster: &ValidatedCluster, + context_names: &ContextNames, + ) -> Labels { + let mut labels = role_selector( + cluster, + &context_names.product_name, + &ValidatedCluster::role_name(), + ); + + labels.insert(Self::build_node_role_label( + &v1alpha1::NodeRole::ClusterManager, + )); + + labels + } + + fn build_node_role_label(node_role: &v1alpha1::NodeRole) -> Label { + // TODO Check the maximum length at compile-time + Label::try_from(( + format!("stackable.tech/opensearch-role.{node_role}"), + "true".to_string(), + )) + .expect("should be a valid label") + } + fn build_container(&self, role_group_config: &OpenSearchRoleGroupConfig) -> Container { let product_image = self .cluster diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 569fb21..d171898 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -9,13 +9,14 @@ use stackable_operator::{ use strum::{EnumDiscriminants, IntoStaticStr}; use super::{ - ContextNames, ProductVersion, RoleGroupName, ValidatedCluster, ValidatedOpenSearchConfig, + ContextNames, OpenSearchRoleGroupConfig, ProductVersion, RoleGroupName, ValidatedCluster, + ValidatedOpenSearchConfig, }; use crate::{ - crd::v1alpha1::{self, OpenSearchConfig}, + crd::v1alpha1::{self, OpenSearchConfig, OpenSearchConfigFragment}, framework::{ ClusterName, - role_utils::{RoleGroupConfig, with_validated_config}, + role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig, with_validated_config}, }, }; @@ -54,10 +55,9 @@ pub enum Error { type Result = std::result::Result; -// TODO split // no client needed pub fn validate( - names: &ContextNames, + context_names: &ContextNames, cluster: &v1alpha1::OpenSearchCluster, ) -> Result { let raw_cluster_name = cluster.meta().name.clone().context(GetClusterNameSnafu)?; @@ -75,43 +75,8 @@ pub fn validate( let role_group_name = RoleGroupName::from_str(raw_role_group_name).context(ParseRoleGroupNameSnafu)?; - let merged_role_group: RoleGroup = with_validated_config( - role_group_config, - &cluster.spec.nodes, - &v1alpha1::OpenSearchConfig::default_config( - &names.product_name, - &cluster_name, - &ValidatedCluster::role_name(), - ), - ) - .context(ValidateOpenSearchConfigSnafu)?; - - let graceful_shutdown_timeout = merged_role_group.config.config.graceful_shutdown_timeout; - - let termination_grace_period_seconds = graceful_shutdown_timeout - .as_secs() - .try_into() - .context(TerminationGracePeriodTooLongSnafu { - duration: graceful_shutdown_timeout, - })?; - - let validated_config = ValidatedOpenSearchConfig { - affinity: merged_role_group.config.config.affinity, - node_roles: merged_role_group.config.config.node_roles, - resources: merged_role_group.config.config.resources, - termination_grace_period_seconds, - }; - - let validated_role_group_config = RoleGroupConfig { - // Kubernetes defaults to 1 if not set - replicas: merged_role_group.replicas.unwrap_or(1), - config: validated_config, - config_overrides: merged_role_group.config.config_overrides, - env_overrides: merged_role_group.config.env_overrides, - cli_overrides: merged_role_group.config.cli_overrides, - pod_overrides: merged_role_group.config.pod_overrides, - product_specific_common_config: merged_role_group.config.product_specific_common_config, - }; + let validated_role_group_config = + validate_role_group_config(context_names, &cluster_name, cluster, role_group_config)?; role_group_configs.insert(role_group_name, validated_role_group_config); } @@ -127,3 +92,46 @@ pub fn validate( role_group_configs, }) } + +fn validate_role_group_config( + context_names: &ContextNames, + cluster_name: &ClusterName, + cluster: &v1alpha1::OpenSearchCluster, + role_group_config: &RoleGroup, +) -> Result { + let merged_role_group: RoleGroup = with_validated_config( + role_group_config, + &cluster.spec.nodes, + &v1alpha1::OpenSearchConfig::default_config( + &context_names.product_name, + cluster_name, + &ValidatedCluster::role_name(), + ), + ) + .context(ValidateOpenSearchConfigSnafu)?; + + let graceful_shutdown_timeout = merged_role_group.config.config.graceful_shutdown_timeout; + let termination_grace_period_seconds = graceful_shutdown_timeout.as_secs().try_into().context( + TerminationGracePeriodTooLongSnafu { + duration: graceful_shutdown_timeout, + }, + )?; + + let validated_config = ValidatedOpenSearchConfig { + affinity: merged_role_group.config.config.affinity, + node_roles: merged_role_group.config.config.node_roles, + resources: merged_role_group.config.config.resources, + termination_grace_period_seconds, + }; + + Ok(RoleGroupConfig { + // Kubernetes defaults to 1 if not set + replicas: merged_role_group.replicas.unwrap_or(1), + config: validated_config, + config_overrides: merged_role_group.config.config_overrides, + env_overrides: merged_role_group.config.env_overrides, + cli_overrides: merged_role_group.config.cli_overrides, + pod_overrides: merged_role_group.config.pod_overrides, + product_specific_common_config: merged_role_group.config.product_specific_common_config, + }) +} diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs index 4c1de0e..ce875ea 100644 --- a/rust/operator-binary/src/framework/kvp/label.rs +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -33,6 +33,20 @@ pub fn recommended_labels( .expect("Labels should be created because all given parameters produce valid label values") } +/// Infallible variant of `Labels::role_selector` +pub fn role_selector( + owner: &(impl Resource + IsLabelValue), + product_name: &ProductName, + role_name: &RoleName, +) -> Labels { + Labels::role_selector( + owner, + &product_name.to_label_value(), + &role_name.to_label_value(), + ) + .expect("Labels should be created because all given parameters produce valid label values") +} + /// Infallible variant of `Labels::role_group_selector` pub fn role_group_selector( owner: &(impl Resource + IsLabelValue), diff --git a/tests/templates/kuttl/smoke/10-assert.yaml b/tests/templates/kuttl/smoke/10-assert.yaml index baf3276..eb06201 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml +++ b/tests/templates/kuttl/smoke/10-assert.yaml @@ -43,7 +43,7 @@ spec: app.kubernetes.io/name: opensearch app.kubernetes.io/role-group: cluster-manager app.kubernetes.io/version: 3.0.0 - cluster_manager: "true" + stackable.tech/opensearch-role.cluster_manager: "true" stackable.tech/vendor: Stackable spec: affinity: @@ -203,9 +203,9 @@ spec: app.kubernetes.io/name: opensearch app.kubernetes.io/role-group: data app.kubernetes.io/version: 3.0.0 - data: "true" - ingest: "true" - remote_cluster_client: "true" + stackable.tech/opensearch-role.data: "true" + stackable.tech/opensearch-role.ingest: "true" + stackable.tech/opensearch-role.remote_cluster_client: "true" stackable.tech/vendor: Stackable spec: affinity: @@ -455,6 +455,11 @@ metadata: app.kubernetes.io/version: 3.0.0 stackable.tech/vendor: Stackable name: opensearch + ownerReferences: + - apiVersion: opensearch.stackable.tech/v1alpha1 + controller: true + kind: OpenSearchCluster + name: opensearch spec: ports: - name: http @@ -467,7 +472,10 @@ spec: targetPort: 9300 publishNotReadyAddresses: true selector: - cluster_manager: "true" + app.kubernetes.io/component: nodes + app.kubernetes.io/instance: opensearch + app.kubernetes.io/name: opensearch + stackable.tech/opensearch-role.cluster_manager: "true" type: ClusterIP --- apiVersion: v1 From 8b71df5c442e317125c523d003d577b4ea4340f6 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 09:54:24 +0200 Subject: [PATCH 28/35] Regenerate charts --- .../helm/opensearch-operator/crds/crds.yaml | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 6782010..6772c15 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -99,6 +99,40 @@ spec: config: default: {} properties: + affinity: + default: + nodeAffinity: null + nodeSelector: null + podAffinity: null + podAntiAffinity: null + description: These configuration settings control [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + properties: + nodeAffinity: + description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + nodeSelector: + additionalProperties: + type: string + description: Simple key-value pairs forming a nodeSelector, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + podAffinity: + description: Same as the `spec.affinity.podAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + podAntiAffinity: + description: Same as the `spec.affinity.podAntiAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + gracefulShutdownTimeout: + description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. + nullable: true + type: string nodeRoles: items: enum: @@ -111,6 +145,90 @@ spec: type: string nullable: true type: array + resources: + default: + cpu: + max: null + min: null + memory: + limit: null + runtimeLimits: {} + storage: + data: + capacity: null + description: Resource usage is configured here, this includes CPU usage, memory usage and disk storage usage, if this role needs any. + properties: + cpu: + default: + max: null + min: null + properties: + max: + description: The maximum amount of CPU cores that can be requested by Pods. Equivalent to the `limit` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + min: + description: The minimal amount of CPU cores that Pods need to run. Equivalent to the `request` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + type: object + memory: + properties: + limit: + description: 'The maximum amount of memory that should be available to the Pod. Specified as a byte [Quantity](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/), which means these suffixes are supported: E, P, T, G, M, k. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki. For example, the following represent roughly the same value: `128974848, 129e6, 129M, 128974848000m, 123Mi`' + nullable: true + type: string + runtimeLimits: + description: Additional options that can be specified. + type: object + type: object + storage: + properties: + data: + default: + capacity: null + properties: + capacity: + description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` ::= \n\n\t(Note that may be empty, from the \"\" case in .)\n\n ::= 0 | 1 | ... | 9 ::= | ::= | . | . | . ::= \"+\" | \"-\" ::= | ::= | | ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n ::= \"e\" | \"E\" ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation." + nullable: true + type: string + selectors: + description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + storageClass: + nullable: true + type: string + type: object + type: object + type: object type: object configOverrides: additionalProperties: @@ -172,6 +290,40 @@ spec: config: default: {} properties: + affinity: + default: + nodeAffinity: null + nodeSelector: null + podAffinity: null + podAntiAffinity: null + description: These configuration settings control [Pod placement](https://docs.stackable.tech/home/nightly/concepts/operations/pod_placement). + properties: + nodeAffinity: + description: Same as the `spec.affinity.nodeAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + nodeSelector: + additionalProperties: + type: string + description: Simple key-value pairs forming a nodeSelector, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + podAffinity: + description: Same as the `spec.affinity.podAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + podAntiAffinity: + description: Same as the `spec.affinity.podAntiAffinity` field on the Pod, see the [Kubernetes docs](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node) + nullable: true + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + gracefulShutdownTimeout: + description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. + nullable: true + type: string nodeRoles: items: enum: @@ -184,6 +336,90 @@ spec: type: string nullable: true type: array + resources: + default: + cpu: + max: null + min: null + memory: + limit: null + runtimeLimits: {} + storage: + data: + capacity: null + description: Resource usage is configured here, this includes CPU usage, memory usage and disk storage usage, if this role needs any. + properties: + cpu: + default: + max: null + min: null + properties: + max: + description: The maximum amount of CPU cores that can be requested by Pods. Equivalent to the `limit` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + min: + description: The minimal amount of CPU cores that Pods need to run. Equivalent to the `request` for Pod resource configuration. Cores are specified either as a decimal point number or as milli units. For example:`1.5` will be 1.5 cores, also written as `1500m`. + nullable: true + type: string + type: object + memory: + properties: + limit: + description: 'The maximum amount of memory that should be available to the Pod. Specified as a byte [Quantity](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/), which means these suffixes are supported: E, P, T, G, M, k. You can also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki. For example, the following represent roughly the same value: `128974848, 129e6, 129M, 128974848000m, 123Mi`' + nullable: true + type: string + runtimeLimits: + description: Additional options that can be specified. + type: object + type: object + storage: + properties: + data: + default: + capacity: null + properties: + capacity: + description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` ::= \n\n\t(Note that may be empty, from the \"\" case in .)\n\n ::= 0 | 1 | ... | 9 ::= | ::= | . | . | . ::= \"+\" | \"-\" ::= | ::= | | ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n ::= \"e\" | \"E\" ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation." + nullable: true + type: string + selectors: + description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + storageClass: + nullable: true + type: string + type: object + type: object + type: object type: object configOverrides: additionalProperties: From e630b2d1d19a5ddba287d9e2be3ffa16779dbd52 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 09:57:26 +0200 Subject: [PATCH 29/35] Fix RustDoc warnings --- .../src/controller/build/node_config.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 794d3cb..49da561 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -167,7 +167,7 @@ impl NodeConfig { /// /// "zen" is the default if `{DISCOVERY_TYPE}` is not set. /// It is nevertheless explicitly set here. - /// see https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/discovery/DiscoveryModule.java#L88-L89 + /// see /// /// "single-node" disables the bootstrap checks, like validating the JVM and discovery /// configurations. @@ -181,14 +181,14 @@ impl NodeConfig { /// Configuration for `cluster.initial_cluster_manager_nodes` which replaces /// `cluster.initial_master_nodes`, see - /// https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/coordination/ClusterBootstrapService.java#L79-L93. + /// . /// /// According to - /// https://docs.opensearch.org/docs/3.0/install-and-configure/configuring-opensearch/discovery-gateway-settings/, + /// , /// it contains "a list of cluster-manager-eligible nodes used to bootstrap the cluster." /// /// However, the documentation for Elasticsearch is more detailed and contains the following - /// notes (see https://www.elastic.co/guide/en/elasticsearch/reference/9.0/modules-discovery-settings.html): + /// notes (see ): /// * Remove this setting once the cluster has formed, and never set it again for this cluster. /// * Do not configure this setting on master-ineligible nodes. /// * Do not configure this setting on nodes joining an existing cluster. @@ -196,7 +196,7 @@ impl NodeConfig { /// * Do not configure this setting when performing a full-cluster restart. /// /// The OpenSearch Helm chart only sets master nodes but does not handle the other cases (see - /// https://github.com/opensearch-project/helm-charts/blob/opensearch-3.0.0/charts/opensearch/templates/statefulset.yaml#L414-L415), + /// ), /// so they are also ignored here for the moment. fn initial_cluster_manager_nodes(&self) -> String { if !self.cluster.is_single_node() @@ -231,7 +231,7 @@ impl NodeConfig { pod_names.join(",") } else { // This setting is not allowed on single node cluster, see - // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/coordination/ClusterBootstrapService.java#L126-L136 + // String::new() } } From 0f10a96647f9d28e29e0716bad1b32d05db0aeef Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 10:13:01 +0200 Subject: [PATCH 30/35] Remove unnecessary comments from Helm values --- deploy/helm/opensearch-operator/values.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/deploy/helm/opensearch-operator/values.yaml b/deploy/helm/opensearch-operator/values.yaml index 1d33135..c1f763f 100644 --- a/deploy/helm/opensearch-operator/values.yaml +++ b/deploy/helm/opensearch-operator/values.yaml @@ -24,15 +24,8 @@ labels: stackable.tech/vendor: Stackable podSecurityContext: {} - # fsGroup: 2000 securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 resources: limits: From e31745757a416f944293d144f32c1533f5b01c07 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 10:44:42 +0200 Subject: [PATCH 31/35] Add changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8f0b5b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +### Added + +- Basic operator for OpenSearch 3.x with the following configuration options ([#10]): + - Cluster operations like `reconciliationPaused` and `stopped` + - Image selection (defaults to the official OpenSearch image for now) + - Overrides (CLI, config, environment variables, Pod) + - Affinities + - Graceful shutdown timeout + - OpenSearch node roles + - Resources (CPU, memory, storage) + - PodDisruptionBudgets + - Replicas + +[#10]: https://github.com/stackabletech/opensearch-operator/pull/10 From b801ecad2130a0d593e8b0212ece9f3073e7fdce Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 10:46:25 +0200 Subject: [PATCH 32/35] Add license --- LICENSE | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b9535c --- /dev/null +++ b/LICENSE @@ -0,0 +1,43 @@ +Licensed under the Open Software License version 3.0 + +1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + +a) to reproduce the Original Work in copies, either alone or as part of a collective work; + +b) to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + +c) to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + +d) to perform the Original Work publicly; and + +e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + +5) External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + +6) Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + +9) Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + +10) Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + +12) Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + +13) Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + +16) Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. From f634420e0ffd3be148a799f53a2e688e4786f85c Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 10:46:45 +0200 Subject: [PATCH 33/35] Add actionlint file --- .actionlint.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .actionlint.yaml diff --git a/.actionlint.yaml b/.actionlint.yaml new file mode 100644 index 0000000..8337548 --- /dev/null +++ b/.actionlint.yaml @@ -0,0 +1,5 @@ +--- +self-hosted-runner: + # Ubicloud machines we are using + labels: + - ubicloud-standard-8-arm From 0fe69ef0bf14efb5d17359ca47946efa92a883fc Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 10:49:47 +0200 Subject: [PATCH 34/35] Add config-spec/properties.yaml --- deploy/config-spec/properties.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 deploy/config-spec/properties.yaml diff --git a/deploy/config-spec/properties.yaml b/deploy/config-spec/properties.yaml new file mode 100644 index 0000000..64765f5 --- /dev/null +++ b/deploy/config-spec/properties.yaml @@ -0,0 +1,4 @@ +version: 0.1.0 +spec: + units: [] +properties: [] From c1b24eb2a536373f4224938c2eed76353f6f4a62 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 15 Jul 2025 11:04:46 +0200 Subject: [PATCH 35/35] Fix yamllint warnings --- deploy/config-spec/properties.yaml | 1 + tests/templates/kuttl/smoke/20-test-opensearch.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/config-spec/properties.yaml b/deploy/config-spec/properties.yaml index 64765f5..9bd8c3b 100644 --- a/deploy/config-spec/properties.yaml +++ b/deploy/config-spec/properties.yaml @@ -1,3 +1,4 @@ +--- version: 0.1.0 spec: units: [] diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml index cb6b515..25c927c 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml @@ -31,7 +31,7 @@ spec: allowPrivilegeEscalation: false capabilities: drop: - - ALL + - ALL runAsNonRoot: true resources: requests: