diff --git a/deploy/helm/opensearch-operator/configs/properties.yaml b/deploy/helm/opensearch-operator/configs/properties.yaml deleted file mode 100644 index 9bd8c3b..0000000 --- a/deploy/helm/opensearch-operator/configs/properties.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 0.1.0 -spec: - units: [] -properties: [] diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 12f3da0..8ee20bc 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -29,9 +29,24 @@ spec: generates in the [operator documentation](https://docs.stackable.tech/home/nightly/opensearch/). properties: clusterConfig: - default: {} + default: + tls: + secretClass: null description: Configuration that applies to all roles and role groups properties: + tls: + properties: + secretClass: + description: |- + Affects client connections and internal transport connections. + This setting controls: + - If TLS encryption is used at all + - Which cert the servers should use to authenticate themselves against the client + maxLength: 223 + minLength: 1 + nullable: true + type: string + type: object vectorAggregatorConfigMapName: description: |- Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery). @@ -42,6 +57,8 @@ spec: minLength: 1 nullable: true type: string + required: + - tls type: object clusterOperation: default: @@ -303,6 +320,14 @@ spec: type: string nullable: true type: array + requestedSecretLifetime: + description: |- + Request secret (currently only autoTls certificates) lifetime from the secret operator, e.g. `7d`, or `30d`. + This can be shortened by the `maxCertificateLifetime` setting on the SecretClass issuing the TLS certificate. + + Defaults to 1d. + nullable: true + type: string resources: default: cpu: @@ -651,6 +676,14 @@ spec: type: string nullable: true type: array + requestedSecretLifetime: + description: |- + Request secret (currently only autoTls certificates) lifetime from the secret operator, e.g. `7d`, or `30d`. + This can be shortened by the `maxCertificateLifetime` setting on the SecretClass issuing the TLS certificate. + + Defaults to 1d. + nullable: true + type: string resources: default: cpu: diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 832e488..3c3a88d 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -30,7 +30,7 @@ use validate::validate; use crate::{ crd::{ NodeRoles, - v1alpha1::{self}, + v1alpha1::{self, OpenSearchClusterConfig}, }, framework::{ ClusterName, ControllerName, HasName, HasUid, ListenerClassName, NameIsValidLabelValue, @@ -131,6 +131,7 @@ pub struct ValidatedOpenSearchConfig { pub listener_class: ListenerClassName, pub logging: ValidatedLogging, pub node_roles: NodeRoles, + pub requested_secret_lifetime: Duration, pub resources: OpenSearchNodeResources, pub termination_grace_period_seconds: i64, } @@ -164,17 +165,20 @@ pub struct ValidatedCluster { pub name: ClusterName, pub namespace: NamespaceName, pub uid: Uid, + pub cluster_config: OpenSearchClusterConfig, pub role_config: GenericRoleConfig, pub role_group_configs: BTreeMap, } impl ValidatedCluster { + #[allow(clippy::too_many_arguments)] pub fn new( image: ResolvedProductImage, product_version: ProductVersion, name: ClusterName, namespace: NamespaceName, uid: impl Into, + cluster_config: OpenSearchClusterConfig, role_config: GenericRoleConfig, role_group_configs: BTreeMap, ) -> Self { @@ -191,6 +195,7 @@ impl ValidatedCluster { name, namespace, uid, + cluster_config, role_config, role_group_configs, } @@ -372,13 +377,17 @@ mod tests { kvp::LabelValue, product_logging::spec::AutomaticContainerLogConfig, role_utils::GenericRoleConfig, + shared::time::Duration, }; use uuid::uuid; use super::{Context, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedLogging}; use crate::{ controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig}, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchClusterConfig}, + }, framework::{ ClusterName, ListenerClassName, NamespaceName, OperatorName, ProductVersion, RoleGroupName, builder::pod::container::EnvVarSet, @@ -460,6 +469,7 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch"), NamespaceName::from_str_unsafe("default"), uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), + OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [ ( @@ -513,6 +523,8 @@ mod tests { vector_container: None, }, node_roles: NodeRoles(node_roles.to_vec()), + requested_secret_lifetime: Duration::from_str("15d") + .expect("should be a valid duration"), resources: OpenSearchNodeResources::default(), termination_grace_period_seconds: 120, }, diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 43c3bda..bc1da16 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -68,6 +68,7 @@ mod tests { kvp::LabelValue, product_logging::spec::AutomaticContainerLogConfig, role_utils::GenericRoleConfig, + shared::time::Duration, }; use uuid::uuid; @@ -77,7 +78,10 @@ mod tests { ContextNames, OpenSearchNodeResources, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchClusterConfig}, + }, framework::{ ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, ProductName, ProductVersion, RoleGroupName, builder::pod::container::EnvVarSet, @@ -168,6 +172,7 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch"), NamespaceName::from_str_unsafe("default"), uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), + OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [ ( @@ -210,6 +215,8 @@ mod tests { vector_container: None, }, node_roles: NodeRoles(node_roles.to_vec()), + requested_secret_lifetime: Duration::from_str("15d") + .expect("should be a valid duration"), resources: OpenSearchNodeResources::default(), termination_grace_period_seconds: 120, }, diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index db795f7..d7cb717 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -58,6 +58,41 @@ pub const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.node pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED: &str = "plugins.security.ssl.http.enabled"; +/// Path to the cert PEM file used for TLS on the HTTP PORT. +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH: &str = + "plugins.security.ssl.http.pemcert_filepath"; + +/// Path to the key PEM file used for TLS on the HTTP PORT. +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH: &str = + "plugins.security.ssl.http.pemkey_filepath"; + +/// Path to the trusted CAs PEM file used for TLS on the HTTP PORT. +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH: &str = + "plugins.security.ssl.http.pemtrustedcas_filepath"; + +/// Whether to enable TLS on internal node-to-node communication using the transport port. +/// type: boolean +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED: &str = + "plugins.security.ssl.transport.enabled"; + +/// Path to the cert PEM file used for TLS on the transport PORT. +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH: &str = + "plugins.security.ssl.transport.pemcert_filepath"; + +/// Path to the key PEM file used for TLS on the transport PORT. +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH: &str = + "plugins.security.ssl.transport.pemkey_filepath"; + +/// Path to the trusted CAs PEM file used for TLS on the transport PORT. +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH: &str = + "plugins.security.ssl.transport.pemtrustedcas_filepath"; + /// Configuration of an OpenSearch node based on the cluster and role-group configuration pub struct NodeConfig { cluster: ValidatedCluster, @@ -81,8 +116,31 @@ impl NodeConfig { } /// Creates the main OpenSearch configuration file in YAML format - pub fn static_opensearch_config_file_content(&self) -> String { - Self::to_yaml(self.static_opensearch_config()) + pub fn opensearch_config_file_content(&self) -> String { + Self::to_yaml(self.opensearch_config()) + } + + pub fn opensearch_config(&self) -> serde_json::Map { + let mut config = self.static_opensearch_config(); + + if self.cluster.cluster_config.tls.secret_class.is_some() { + config.append(&mut self.tls_config()); + } + + 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(); + + config } /// Creates the main OpenSearch configuration file as JSON map @@ -112,25 +170,53 @@ impl NodeConfig { 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)); - } + config + } - // Ensure a deterministic result - config.sort_keys(); + pub fn tls_config(&self) -> serde_json::Map { + let mut config = serde_json::Map::new(); + + // TLS config for HTTP port + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!("true".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), + json!("/stackable/tls/tls.crt".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), + json!("/stackable/tls/tls.key".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!("/stackable/tls/ca.crt".to_string()), + ); + // TLS config for TRANSPORT port + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED.to_owned(), + json!("true".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), + json!("/stackable/tls/tls.crt".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), + json!("/stackable/tls/tls.key".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!("/stackable/tls/ca.crt".to_string()), + ); config } /// Returns `true` if TLS is enabled on the HTTP port pub fn tls_on_http_port_enabled(&self) -> bool { - self.static_opensearch_config() + self.opensearch_config() .get(CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED) .and_then(Self::value_as_bool) == Some(true) @@ -277,13 +363,14 @@ mod tests { kvp::LabelValue, product_logging::spec::AutomaticContainerLogConfig, role_utils::GenericRoleConfig, + shared::time::Duration, }; use uuid::uuid; use super::*; use crate::{ controller::{ValidatedLogging, ValidatedOpenSearchConfig}, - crd::NodeRoles, + crd::{NodeRoles, v1alpha1::OpenSearchClusterConfig}, framework::{ ClusterName, ListenerClassName, NamespaceName, ProductVersion, RoleGroupName, product_logging::framework::ValidatedContainerLogConfigChoice, @@ -328,6 +415,8 @@ mod tests { v1alpha1::NodeRole::Ingest, v1alpha1::NodeRole::RemoteClusterClient, ]), + requested_secret_lifetime: Duration::from_str("15d") + .expect("should be a valid duration"), resources: Resources::default(), termination_grace_period_seconds: 30, }, @@ -364,6 +453,7 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch-cluster"), NamespaceName::from_str_unsafe("default"), uuid!("0b1e30e6-326e-4c1a-868d-ad6598b49e8b"), + OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), @@ -395,7 +485,7 @@ mod tests { "test: \"value\"" ) .to_owned(), - node_config.static_opensearch_config_file_content() + node_config.opensearch_config_file_content() ); } diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 454d7d5..3199817 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -230,6 +230,7 @@ mod tests { kvp::LabelValue, product_logging::spec::AutomaticContainerLogConfig, role_utils::GenericRoleConfig, + shared::time::Duration, }; use uuid::uuid; @@ -239,7 +240,10 @@ mod tests { ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchClusterConfig}, + }, framework::{ ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, ProductName, ProductVersion, RoleGroupName, builder::pod::container::EnvVarSet, @@ -276,6 +280,8 @@ mod tests { v1alpha1::NodeRole::Ingest, v1alpha1::NodeRole::RemoteClusterClient, ]), + requested_secret_lifetime: Duration::from_str("15d") + .expect("should be a valid duration"), resources: Resources::default(), termination_grace_period_seconds: 30, }, @@ -299,6 +305,7 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch-cluster"), NamespaceName::from_str_unsafe("default"), uuid!("0b1e30e6-326e-4c1a-868d-ad6598b49e8b"), + OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("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 431690f..363a1e1 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, str::FromStr}; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, + builder::{meta::ObjectMetaBuilder, pod::volume::SecretFormat}, crd::listener::{self}, k8s_openapi::{ DeepMerge, @@ -49,6 +49,7 @@ use crate::{ container::{EnvVarName, new_container_builder}, volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, }, + volume::build_tls_volume, }, kvp::label::{recommended_labels, role_group_selector, role_selector}, product_logging::framework::{ @@ -70,6 +71,8 @@ constant!(DATA_VOLUME_NAME: VolumeName = "data"); constant!(LISTENER_VOLUME_NAME: PersistentVolumeClaimName = "listener"); const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; +constant!(TLS_VOLUME_NAME: VolumeName = "tls"); +const TLS_VOLUME_DIR: &str = "/stackable/tls"; constant!(LOG_VOLUME_NAME: VolumeName = "log"); const LOG_VOLUME_DIR: &str = "/stackable/log"; @@ -126,7 +129,7 @@ impl<'a> RoleGroupBuilder<'a> { data.insert( CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), - self.node_config.static_opensearch_config_file_content(), + self.node_config.opensearch_config_file_content(), ); if let ValidatedContainerLogConfigChoice::Automatic(log_config) = @@ -212,6 +215,8 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the [`PodTemplateSpec`] for the role-group [`StatefulSet`] fn build_pod_template(&self) -> PodTemplateSpec { let mut node_role_labels = Labels::new(); + let service_scopes = vec![self.resource_names.headless_service_name()]; + for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels.insert(Self::build_node_role_label(node_role)); } @@ -256,7 +261,9 @@ impl<'a> RoleGroupBuilder<'a> { self.resource_names.role_group_config_map() }; - let volumes = vec![ + let requested_secret_lifetime = self.role_group_config.config.requested_secret_lifetime; + + let mut volumes = vec![ Volume { name: CONFIG_VOLUME_NAME.to_string(), config_map: Some(ConfigMapVolumeSource { @@ -287,6 +294,17 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; + if let Some(tls_secret_class_name) = &self.cluster.cluster_config.tls.secret_class { + volumes.push(build_tls_volume( + &TLS_VOLUME_NAME.to_string(), + tls_secret_class_name, + service_scopes, + SecretFormat::TlsPem, + &requested_secret_lifetime, + Some(&LISTENER_VOLUME_NAME.to_string()), + )) + }; + // 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, @@ -406,7 +424,7 @@ impl<'a> RoleGroupBuilder<'a> { .and_then(|env_var| env_var.value.clone()) .unwrap_or(format!("{opensearch_home}/config")); - let volume_mounts = [ + let mut volume_mounts = vec![ VolumeMount { mount_path: format!("{opensearch_path_conf}/{CONFIGURATION_FILE_OPENSEARCH_YML}"), name: CONFIG_VOLUME_NAME.to_string(), @@ -440,6 +458,14 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; + if self.cluster.cluster_config.tls.secret_class.is_some() { + volume_mounts.push(VolumeMount { + mount_path: TLS_VOLUME_DIR.to_owned(), + name: TLS_VOLUME_NAME.to_string(), + ..VolumeMount::default() + }) + } + new_container_builder(&v1alpha1::Container::OpenSearch.to_container_name()) .image_from_product_image(&self.cluster.image) .command(vec![ @@ -647,6 +673,7 @@ mod tests { kvp::LabelValue, product_logging::spec::AutomaticContainerLogConfig, role_utils::GenericRoleConfig, + shared::time::Duration, }; use strum::IntoEnumIterator; use uuid::uuid; @@ -660,7 +687,10 @@ mod tests { ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchClusterConfig}, + }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, OperatorName, ProductName, ProductVersion, RoleGroupName, ServiceAccountName, @@ -722,6 +752,8 @@ mod tests { v1alpha1::NodeRole::Ingest, v1alpha1::NodeRole::RemoteClusterClient, ]), + requested_secret_lifetime: Duration::from_str("15d") + .expect("should be a valid duration"), resources: Resources::default(), termination_grace_period_seconds: 30, }, @@ -738,6 +770,7 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch-cluster"), NamespaceName::from_str_unsafe("default"), uuid!("0b1e30e6-326e-4c1a-868d-ad6598b49e8b"), + OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index e3dd839..e878b4d 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -74,6 +74,9 @@ pub enum Error { source: crate::framework::product_logging::framework::Error, }, + #[snafu(display("failed to set tls secret class"))] + ParseTlsSecretClassName { source: crate::framework::Error }, + #[snafu(display("fragment validation failure"))] ValidateOpenSearchConfig { source: stackable_operator::config::fragment::ValidationError, @@ -138,6 +141,7 @@ pub fn validate( cluster_name, namespace, uid, + cluster.spec.cluster_config.clone(), cluster.spec.nodes.role_config.clone(), role_group_configs, )) @@ -183,6 +187,7 @@ fn validate_role_group_config( listener_class: merged_role_group.config.config.listener_class, logging, node_roles: merged_role_group.config.config.node_roles, + requested_secret_lifetime: merged_role_group.config.config.requested_secret_lifetime, resources: merged_role_group.config.config.resources, termination_grace_period_seconds, }; @@ -273,11 +278,11 @@ mod tests { controller::{ContextNames, ValidatedCluster, ValidatedLogging, ValidatedOpenSearchConfig}, crd::{ NodeRoles, - v1alpha1::{self}, + v1alpha1::{self, OpenSearchClusterConfig, OpenSearchTls}, }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, - OperatorName, ProductName, ProductVersion, RoleGroupName, + OperatorName, ProductName, ProductVersion, RoleGroupName, TlsSecretClassName, builder::pod::container::{EnvVarName, EnvVarSet}, product_logging::framework::{ ValidatedContainerLogConfigChoice, VectorContainerLogConfig, @@ -304,6 +309,14 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch"), NamespaceName::from_str_unsafe("default"), uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), + OpenSearchClusterConfig { + tls: OpenSearchTls { + secret_class: Some(TlsSecretClassName::from_str_unsafe("tls")) + }, + vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( + "vector-aggregator" + )) + }, GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), @@ -400,6 +413,8 @@ mod tests { ] .into() ), + requested_secret_lifetime: Duration::from_str("15d") + .expect("should be a valid duration"), resources: Resources { memory: MemoryLimits { limit: Some(Quantity("2Gi".to_owned())), @@ -662,6 +677,9 @@ mod tests { image: serde_json::from_str(r#"{"productVersion": "3.1.0"}"#) .expect("should be a valid ProductImage structure"), cluster_config: v1alpha1::OpenSearchClusterConfig { + tls: OpenSearchTls { + secret_class: Some(TlsSecretClassName::from_str_unsafe("tls")), + }, vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( "vector-aggregator", )), diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 4803e70..92b399a 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -30,7 +30,7 @@ use crate::{ constant, framework::{ ClusterName, ConfigMapName, ContainerName, ListenerClassName, NameIsValidLabelValue, - ProductName, RoleName, role_utils::GenericProductSpecificCommonConfig, + ProductName, RoleName, TlsSecretClassName, role_utils::GenericProductSpecificCommonConfig, }, }; @@ -80,6 +80,7 @@ pub mod versioned { #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct OpenSearchClusterConfig { + pub tls: OpenSearchTls, /// Name of the Vector aggregator [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery). /// It must contain the key `ADDRESS` with the address of the Vector aggregator. /// Follow the [logging tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/logging-vector-aggregator) @@ -88,6 +89,16 @@ pub mod versioned { pub vector_aggregator_config_map_name: Option, } + #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct OpenSearchTls { + /// Affects client connections and internal transport connections. + /// This setting controls: + /// - If TLS encryption is used at all + /// - Which cert the servers should use to authenticate themselves against the client + pub secret_class: Option, + } + // 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. // @@ -168,6 +179,13 @@ pub mod versioned { /// documentation](DOCS_BASE_URL_PLACEHOLDER/opensearch/usage-guide/node-roles) for details. pub node_roles: NodeRoles, + /// Request secret (currently only autoTls certificates) lifetime from the secret operator, e.g. `7d`, or `30d`. + /// This can be shortened by the `maxCertificateLifetime` setting on the SecretClass issuing the TLS certificate. + /// + /// Defaults to 1d. + #[fragment_attrs(serde(default))] + pub requested_secret_lifetime: Duration, + #[fragment_attrs(serde(default))] pub resources: Resources, } @@ -270,6 +288,9 @@ impl v1alpha1::OpenSearchConfig { v1alpha1::NodeRole::Data, v1alpha1::NodeRole::RemoteClusterClient, ])), + requested_secret_lifetime: Some( + Duration::from_str("15d").expect("should be a valid duration"), + ), resources: ResourcesFragment { memory: MemoryLimitsFragment { // An idle node already requires 2 Gi. diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 3442b2e..9b03c46 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -79,6 +79,9 @@ pub enum Error { /// Duplicates the private constant [`stackable-operator::kvp::label::value::LABEL_VALUE_MAX_LEN`] pub const MAX_LABEL_VALUE_LENGTH: usize = 63; +/// Maximum length of annotation values +pub const MAX_ANNOTATION_LENGTH: usize = 253; + /// Has a non-empty name /// /// Useful as an object reference; Should not be used to create an object because the name could @@ -512,7 +515,13 @@ attributed_string_type! { is_rfc_1123_label_name, is_valid_label_value } - +attributed_string_type! { + TlsSecretClassName, + "The TLS SecretClass name", + "tls", + // The secret class name is used in an annotation on the tls volume. To make sure the + (max_length = MAX_ANNOTATION_LENGTH - 30) +} #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/rust/operator-binary/src/framework/builder.rs b/rust/operator-binary/src/framework/builder.rs index 40caba1..078acfa 100644 --- a/rust/operator-binary/src/framework/builder.rs +++ b/rust/operator-binary/src/framework/builder.rs @@ -1,3 +1,4 @@ pub mod meta; pub mod pdb; pub mod pod; +pub mod volume; diff --git a/rust/operator-binary/src/framework/builder/volume.rs b/rust/operator-binary/src/framework/builder/volume.rs new file mode 100644 index 0000000..b0ac896 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/volume.rs @@ -0,0 +1,37 @@ +use stackable_operator::{ + builder::pod::volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + k8s_openapi::api::core::v1::Volume, + shared::time::Duration, +}; + +use crate::framework::TlsSecretClassName; + +pub fn build_tls_volume( + volume_name: &String, + tls_secret_class_name: &TlsSecretClassName, + service_scopes: impl IntoIterator>, + secret_format: SecretFormat, + requested_secret_lifetime: &Duration, + listener_scope: Option<&str>, +) -> Volume { + let mut secret_volume_source_builder = + SecretOperatorVolumeSourceBuilder::new(tls_secret_class_name); + + for scope in service_scopes { + secret_volume_source_builder.with_service_scope(scope.as_ref()); + } + if let Some(listener_scope) = listener_scope { + secret_volume_source_builder.with_listener_volume_scope(listener_scope); + } + + VolumeBuilder::new(volume_name) + .ephemeral( + secret_volume_source_builder + .with_pod_scope() + .with_format(secret_format) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .expect("volume should be built without parse errors"), + ) + .build() +}