diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a6d74..5767688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,10 @@ All notable changes to this project will be documented in this file. ### Added - Add the role group as a node attribute ([#63]). +- Allow adding entries to the OpenSearch keystore ([#76]). [#63]: https://github.com/stackabletech/opensearch-operator/pull/63 +[#76]: https://github.com/stackabletech/opensearch-operator/pull/76 ## [25.11.0] - 2025-11-07 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 50448f6..fcf1f11 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -29,9 +29,44 @@ spec: generates in the [operator documentation](https://docs.stackable.tech/home/nightly/opensearch/). properties: clusterConfig: - default: {} + default: + keystore: [] description: Configuration that applies to all roles and role groups properties: + keystore: + default: [] + description: Entries to add to the OpenSearch keystore. + items: + properties: + key: + description: Key in the OpenSearch keystore + minLength: 1 + pattern: ^[A-Za-z0-9_\-.]+$ + type: string + secretKeyRef: + description: Reference to the Secret containing the value which will be stored in the OpenSearch keystore + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + required: + - key + - secretKeyRef + type: object + type: array vectorAggregatorConfigMapName: description: |- Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery). diff --git a/docs/modules/opensearch/pages/usage-guide/keystore.adoc b/docs/modules/opensearch/pages/usage-guide/keystore.adoc new file mode 100644 index 0000000..d849cb0 --- /dev/null +++ b/docs/modules/opensearch/pages/usage-guide/keystore.adoc @@ -0,0 +1,40 @@ += Add entries to the OpenSearch Keystore +:description: Add entries to the OpenSearch Keystore + +The OpenSearch keystore provides secure storage for sensitive configuration settings such as credentials and API keys. +You can populate the keystore by referencing Secrets within your OpenSearch configuration. + +[source,yaml] +---- +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + clusterConfig: + keystore: + - key: s3.client.default.access_key # <1> + secretKeyRef: + name: s3-credentials # <2> + key: accessKey # <3> + - key: s3.client.default.secret_key + secretKeyRef: + name: s3-credentials + key: secretKey + nodes: + roleGroups: + default: + replicas: 1 +--- +apiVersion: v1 +kind: Secret +metadata: + name: s3-credentials +stringData: + accessKey: my-access-key + secretKey: my-secret-key +---- +<1> The key in the OpenSearch keystore which corresponds to a setting in OpenSearch (e.g. `s3.client.default.access_key`). +<2> The name of the Secret containing the value +<3> The key within that Secret diff --git a/docs/modules/opensearch/partials/nav.adoc b/docs/modules/opensearch/partials/nav.adoc index 1994a6e..5853f30 100644 --- a/docs/modules/opensearch/partials/nav.adoc +++ b/docs/modules/opensearch/partials/nav.adoc @@ -10,6 +10,7 @@ ** xref:opensearch:usage-guide/logging.adoc[] ** xref:opensearch:usage-guide/opensearch-dashboards.adoc[] ** xref:opensearch:usage-guide/scaling.adoc[] +** xref:opensearch:usage-guide/keystore.adoc[] ** xref:opensearch:usage-guide/operations/index.adoc[] *** xref:opensearch:usage-guide/operations/cluster-operations.adoc[] *** xref:opensearch:usage-guide/operations/pod-placement.adoc[] diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 3a6d210..b68fe7f 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -28,10 +28,7 @@ use update_status::update_status; use validate::validate; use crate::{ - crd::{ - NodeRoles, - v1alpha1::{self}, - }, + crd::{NodeRoles, v1alpha1}, framework::{ HasName, HasUid, NameIsValidLabelValue, product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig}, @@ -172,9 +169,11 @@ pub struct ValidatedCluster { pub uid: Uid, pub role_config: GenericRoleConfig, pub role_group_configs: BTreeMap, + pub keystores: Vec, } impl ValidatedCluster { + #[allow(clippy::too_many_arguments)] pub fn new( image: ResolvedProductImage, product_version: ProductVersion, @@ -183,6 +182,7 @@ impl ValidatedCluster { uid: impl Into, role_config: GenericRoleConfig, role_group_configs: BTreeMap, + keystores: Vec, ) -> Self { let uid = uid.into(); ValidatedCluster { @@ -199,6 +199,7 @@ impl ValidatedCluster { uid, role_config, role_group_configs, + keystores, } } @@ -384,13 +385,16 @@ mod tests { use super::{Context, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedLogging}; use crate::{ controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig}, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, OpenSearchKeystoreKey, + v1alpha1::{self, OpenSearchKeystore, SecretKeyRef}, + }, framework::{ builder::pod::container::EnvVarSet, product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, types::{ - kubernetes::{ListenerClassName, NamespaceName}, + kubernetes::{ListenerClassName, NamespaceName, SecretKey, SecretName}, operator::{ClusterName, OperatorName, ProductVersion, RoleGroupName}, }, }, @@ -503,6 +507,13 @@ mod tests { ), ] .into(), + vec![OpenSearchKeystore { + key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), + secret_key_ref: SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file"), + }, + }], ) } diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 08a1afb..f689547 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -77,12 +77,12 @@ mod tests { ContextNames, OpenSearchNodeResources, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, framework::{ builder::pod::container::EnvVarSet, role_utils::GenericProductSpecificCommonConfig, types::{ - kubernetes::{ListenerClassName, NamespaceName}, + kubernetes::{ListenerClassName, NamespaceName, SecretKey, SecretName}, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, @@ -197,6 +197,13 @@ mod tests { ), ] .into(), + vec![v1alpha1::OpenSearchKeystore { + key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), + secret_key_ref: v1alpha1::SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file"), + }, + }], ) } diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 6a1aa4b..562dadd 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -62,6 +62,8 @@ 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"; +const DEFAULT_OPENSEARCH_HOME: &str = "/stackable/opensearch"; + /// Configuration of an OpenSearch node based on the cluster and role-group configuration pub struct NodeConfig { cluster: ValidatedCluster, @@ -272,6 +274,23 @@ impl NodeConfig { String::new() } } + + /// Return content of the `OPENSEARCH_HOME` environment variable from envOverrides or default to `DEFAULT_OPENSEARCH_HOME` + pub fn opensearch_home(&self) -> String { + self.environment_variables() + .get(&EnvVarName::from_str_unsafe("OPENSEARCH_HOME")) + .and_then(|env_var| env_var.value.clone()) + .unwrap_or(DEFAULT_OPENSEARCH_HOME.to_owned()) + } + + /// Return content of the `OPENSEARCH_PATH_CONF` environment variable from envOverrides or default to `OPENSEARCH_HOME/config` + pub fn opensearch_path_conf(&self) -> String { + let opensearch_home = self.opensearch_home(); + self.environment_variables() + .get(&EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF")) + .and_then(|env_var| env_var.value.clone()) + .unwrap_or(format!("{opensearch_home}/config")) + } } #[cfg(test)] @@ -386,6 +405,7 @@ mod tests { role_group_config.clone(), )] .into(), + vec![], ); NodeConfig::new( diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index f556909..a63c175 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -311,6 +311,7 @@ mod tests { role_group_config.clone(), )] .into(), + vec![], ); RoleBuilder::new(cluster, context_names) 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 8d8880f..2e543cf 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -11,12 +11,14 @@ use stackable_operator::{ apps::v1::{StatefulSet, StatefulSetSpec}, core::v1::{ Affinity, ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, - EmptyDirVolumeSource, PersistentVolumeClaim, PodSecurityContext, PodSpec, - PodTemplateSpec, Probe, Service, ServicePort, ServiceSpec, TCPSocketAction, Volume, - VolumeMount, + EmptyDirVolumeSource, KeyToPath, PersistentVolumeClaim, PodSecurityContext, + PodSpec, PodTemplateSpec, Probe, SecretVolumeSource, Service, ServicePort, + ServiceSpec, TCPSocketAction, Volume, VolumeMount, }, }, - apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString}, + apimachinery::pkg::{ + api::resource::Quantity, apis::meta::v1::LabelSelector, util::intstr::IntOrString, + }, }, kvp::{Annotation, Annotations, Label, Labels}, product_logging::framework::{ @@ -45,7 +47,7 @@ use crate::{ builder::{ meta::ownerreference_from_resource, pod::{ - container::{EnvVarName, new_container_builder}, + container::new_container_builder, volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, }, }, @@ -77,7 +79,11 @@ const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; constant!(LOG_VOLUME_NAME: VolumeName = "log"); const LOG_VOLUME_DIR: &str = "/stackable/log"; -const DEFAULT_OPENSEARCH_HOME: &str = "/stackable/opensearch"; +const OPENSEARCH_KEYSTORE_FILE_NAME: &str = "opensearch.keystore"; +const OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTORY_NAME: &str = "initialized-keystore"; +const OPENSEARCH_KEYSTORE_SECRETS_DIRECTORY: &str = "keystore-secrets"; +constant!(OPENSEARCH_KEYSTORE_VOLUME_NAME: VolumeName = "keystore"); +const OPENSEARCH_KEYSTORE_VOLUME_SIZE: &str = "1Mi"; /// Builder for role-group resources pub struct RoleGroupBuilder<'a> { @@ -251,6 +257,11 @@ impl<'a> RoleGroupBuilder<'a> { ) }); + let mut init_containers = vec![]; + if let Some(keystore_init_container) = self.build_maybe_keystore_init_container() { + init_containers.push(keystore_init_container); + } + let log_config_volume_config_map = if let ValidatedContainerLogConfigChoice::Custom(config_map_name) = &self.role_group_config.config.logging.opensearch_container @@ -260,7 +271,7 @@ impl<'a> RoleGroupBuilder<'a> { self.resource_names.role_group_config_map() }; - let volumes = vec![ + let mut volumes = vec![ Volume { name: CONFIG_VOLUME_NAME.to_string(), config_map: Some(ConfigMapVolumeSource { @@ -291,6 +302,34 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; + if !self.cluster.keystores.is_empty() { + volumes.push(Volume { + name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(OPENSEARCH_KEYSTORE_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }) + } + + for (index, keystore) in self.cluster.keystores.iter().enumerate() { + volumes.push(Volume { + name: format!("keystore-{index}"), + secret: Some(SecretVolumeSource { + default_mode: Some(0o660), + secret_name: Some(keystore.secret_key_ref.name.to_string()), + items: Some(vec![KeyToPath { + key: keystore.secret_key_ref.key.to_string(), + path: keystore.secret_key_ref.key.to_string(), + ..KeyToPath::default() + }]), + ..SecretVolumeSource::default() + }), + ..Volume::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, @@ -312,6 +351,7 @@ impl<'a> RoleGroupBuilder<'a> { .into_iter() .flatten() .collect(), + init_containers: Some(init_containers), node_selector: self .role_group_config .config @@ -371,6 +411,58 @@ impl<'a> RoleGroupBuilder<'a> { .expect("should be a valid label") } + /// Builds the container for the [`PodTemplateSpec`] + fn build_maybe_keystore_init_container(&self) -> Option { + if self.cluster.keystores.is_empty() { + return None; + } + let opensearch_home = self.node_config.opensearch_home(); + let mut volume_mounts = vec![VolumeMount { + mount_path: format!( + "{opensearch_home}/{OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTORY_NAME}" + ), + name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), + ..VolumeMount::default() + }]; + + for (index, keystore) in self.cluster.keystores.iter().enumerate() { + volume_mounts.push(VolumeMount { + mount_path: format!( + "{opensearch_home}/{OPENSEARCH_KEYSTORE_SECRETS_DIRECTORY}/{}", + keystore.key + ), + name: format!("keystore-{index}"), + read_only: Some(true), + sub_path: Some(keystore.secret_key_ref.key.to_string()), + ..VolumeMount::default() + }); + } + + Some( + new_container_builder(&v1alpha1::Container::InitKeystore.to_container_name()) + .image_from_product_image(&self.cluster.image) + .command(vec![ + "/bin/bash".to_string(), + "-x".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![format!( + "bin/opensearch-keystore create +for i in keystore-secrets/*; do + key=$(basename $i) + bin/opensearch-keystore add-file \"$key\" \"$i\" +done +cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTORY_NAME}", + )]) + .add_volume_mounts(volume_mounts) + .expect("The mount paths are statically defined and there should be no duplicates.") + .resources(self.role_group_config.config.resources.clone().into()) + .build(), + ) + } + /// Builds the container for the [`PodTemplateSpec`] fn build_opensearch_container(&self) -> Container { // Probe values taken from the official Helm chart @@ -398,19 +490,10 @@ impl<'a> RoleGroupBuilder<'a> { let env_vars = self.node_config.environment_variables(); - // Use `OPENSEARCH_HOME` from envOverrides or default to `DEFAULT_OPENSEARCH_HOME`. - let opensearch_home = env_vars - .get(&EnvVarName::from_str_unsafe("OPENSEARCH_HOME")) - .and_then(|env_var| env_var.value.clone()) - .unwrap_or(DEFAULT_OPENSEARCH_HOME.to_owned()); - // Use `OPENSEARCH_PATH_CONF` from envOverrides or default to `OPENSEARCH_HOME/config`, - // i.e. depend on `OPENSEARCH_HOME`. - let opensearch_path_conf = env_vars - .get(&EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF")) - .and_then(|env_var| env_var.value.clone()) - .unwrap_or(format!("{opensearch_home}/config")); - - let volume_mounts = [ + let opensearch_home = self.node_config.opensearch_home(); + let opensearch_path_conf = self.node_config.opensearch_path_conf(); + + let mut volume_mounts = vec![ VolumeMount { mount_path: format!("{opensearch_path_conf}/{CONFIGURATION_FILE_OPENSEARCH_YML}"), name: CONFIG_VOLUME_NAME.to_string(), @@ -444,6 +527,16 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; + if !self.cluster.keystores.is_empty() { + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/{OPENSEARCH_KEYSTORE_FILE_NAME}"), + name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), + sub_path: Some(OPENSEARCH_KEYSTORE_FILE_NAME.to_owned()), + read_only: Some(true), + ..VolumeMount::default() + }) + } + new_container_builder(&v1alpha1::Container::OpenSearch.to_container_name()) .image_from_product_image(&self.cluster.image) .command(vec![ @@ -664,15 +757,15 @@ mod tests { ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, framework::{ builder::pod::container::EnvVarSet, product_logging::framework::VectorContainerLogConfig, role_utils::GenericProductSpecificCommonConfig, types::{ kubernetes::{ - ConfigMapName, ListenerClassName, NamespaceName, ServiceAccountName, - ServiceName, + ConfigMapName, ListenerClassName, NamespaceName, SecretKey, SecretName, + ServiceAccountName, ServiceName, }, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, @@ -756,6 +849,13 @@ mod tests { role_group_config.clone(), )] .into(), + vec![v1alpha1::OpenSearchKeystore { + key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), + secret_key_ref: v1alpha1::SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file"), + }, + }], ) } @@ -1030,6 +1130,12 @@ mod tests { { "mountPath": "/stackable/log", "name": "log" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch.keystore", + "name": "keystore", + "readOnly": true, + "subPath": "opensearch.keystore", } ] }, @@ -1131,6 +1237,43 @@ mod tests { ], }, ], + "initContainers": [ + { + "args": [ + concat!( + "bin/opensearch-keystore create\n", + "for i in keystore-secrets/*; do\n", + " key=$(basename $i)\n", + " bin/opensearch-keystore add-file \"$key\" \"$i\"\n", + "done\n", + "cp --archive config/opensearch.keystore initialized-keystore" + ), + ], + "command": [ + "/bin/bash", + "-x", + "-euo", + "pipefail", + "-c" + ], + "image": "oci.stackable.tech/sdp/opensearch:3.1.0-stackable0.0.0-dev", + "imagePullPolicy": "Always", + "name": "init-keystore", + "resources": {}, + "volumeMounts": [ + { + "mountPath": "/stackable/opensearch/initialized-keystore", + "name": "keystore", + }, + { + "mountPath": "/stackable/opensearch/keystore-secrets/Keystore1", + "name": "keystore-0", + "readOnly": true, + "subPath": "my-keystore-file" + } + ] + } + ], "securityContext": { "fsGroup": 1000 }, @@ -1156,7 +1299,26 @@ mod tests { "sizeLimit": "30Mi" }, "name": "log" - } + }, + { + "emptyDir": { + "sizeLimit": "1Mi" + }, + "name": "keystore" + }, + { + "name": "keystore-0", + "secret": { + "defaultMode": 0o660, + "items": [ + { + "key": "my-keystore-file", + "path": "my-keystore-file" + } + ], + "secretName": "my-keystore-secret" + } + } ] } }, diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 0f6f10c..a45a409 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -153,6 +153,7 @@ pub fn validate( uid, cluster.spec.nodes.role_config.clone(), role_group_configs, + cluster.spec.cluster_config.keystore.clone(), )) } @@ -285,10 +286,7 @@ mod tests { use crate::{ built_info, controller::{ContextNames, ValidatedCluster, ValidatedLogging, ValidatedOpenSearchConfig}, - crd::{ - NodeRoles, - v1alpha1::{self}, - }, + crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, framework::{ builder::pod::container::{EnvVarName, EnvVarSet}, product_logging::framework::{ @@ -296,7 +294,9 @@ mod tests { }, role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, types::{ - kubernetes::{ConfigMapName, ListenerClassName, NamespaceName}, + kubernetes::{ + ConfigMapName, ListenerClassName, NamespaceName, SecretKey, SecretName, + }, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, @@ -510,6 +510,13 @@ mod tests { } )] .into(), + vec![v1alpha1::OpenSearchKeystore { + key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), + secret_key_ref: v1alpha1::SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file") + } + }] )), result.ok() ); @@ -687,6 +694,13 @@ mod tests { image: serde_json::from_str(r#"{"productVersion": "3.1.0"}"#) .expect("should be a valid ProductImage structure"), cluster_config: v1alpha1::OpenSearchClusterConfig { + keystore: vec![v1alpha1::OpenSearchKeystore { + key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), + secret_key_ref: v1alpha1::SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file"), + }, + }], 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 768ea11..cfa5d55 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -27,12 +27,12 @@ use stackable_operator::{ use strum::{Display, EnumIter}; use crate::{ - constant, + attributed_string_type, constant, framework::{ NameIsValidLabelValue, role_utils::GenericProductSpecificCommonConfig, types::{ - kubernetes::{ConfigMapName, ContainerName, ListenerClassName}, + kubernetes::{ConfigMapName, ContainerName, ListenerClassName, SecretKey, SecretName}, operator::{ClusterName, ProductName, RoleName}, }, }, @@ -84,6 +84,10 @@ pub mod versioned { #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct OpenSearchClusterConfig { + /// Entries to add to the OpenSearch keystore. + #[serde(default)] + pub keystore: Vec, + /// 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) @@ -92,6 +96,24 @@ pub mod versioned { pub vector_aggregator_config_map_name: Option, } + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct OpenSearchKeystore { + /// Key in the OpenSearch keystore + pub key: OpenSearchKeystoreKey, + + /// Reference to the Secret containing the value which will be stored in the OpenSearch keystore + pub secret_key_ref: SecretKeyRef, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + pub struct SecretKeyRef { + /// Name of the Secret + pub name: SecretName, + /// Key in the Secret that contains the value + pub key: SecretKey, + } + // 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. // @@ -195,6 +217,9 @@ pub mod versioned { #[serde(rename = "vector")] Vector, + + #[serde(rename = "init-keystore")] + InitKeystore, } #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] @@ -327,11 +352,21 @@ impl v1alpha1::Container { ContainerName::from_str(match self { v1alpha1::Container::OpenSearch => "opensearch", v1alpha1::Container::Vector => "vector", + v1alpha1::Container::InitKeystore => "init-keystore", }) .expect("should be a valid container name") } } +// See https://github.com/opensearch-project/OpenSearch/blob/8ff7c6ee924a49f0f59f80a6e1c73073c8904214/server/src/main/java/org/opensearch/common/settings/KeyStoreWrapper.java#L125 +attributed_string_type! { + OpenSearchKeystoreKey, + "Key in an OpenSearch keystore", + "s3.client.default.access_key", + (min_length = 1), + (regex = "[A-Za-z0-9_\\-.]+") +} + #[cfg(test)] mod tests { use strum::IntoEnumIterator; diff --git a/tests/templates/kuttl/backup-restore/10-install-s3-credentials-secret.yaml b/tests/templates/kuttl/backup-restore/10-install-s3-credentials-secret.yaml index ee58ffb..9b71b36 100644 --- a/tests/templates/kuttl/backup-restore/10-install-s3-credentials-secret.yaml +++ b/tests/templates/kuttl/backup-restore/10-install-s3-credentials-secret.yaml @@ -4,5 +4,5 @@ kind: Secret metadata: name: s3-credentials stringData: - s3.client.default.access_key: openSearchAccessKey - s3.client.default.secret_key: openSearchSecretKey + ACCESS_KEY: openSearchAccessKey + SECRET_KEY: openSearchSecretKey diff --git a/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 b/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 index fa2e305..2f7dfc5 100644 --- a/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 @@ -10,8 +10,17 @@ spec: {% endif %} productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + keystore: + - key: s3.client.default.access_key + secretKeyRef: + name: s3-credentials + key: ACCESS_KEY + - key: s3.client.default.secret_key + secretKeyRef: + name: s3-credentials + key: SECRET_KEY +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -50,34 +59,6 @@ spec: podOverrides: spec: initContainers: - - name: init-keystore -{% if test_scenario['values']['opensearch'].find(",") > 0 %} - image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" -{% else %} - image: oci.stackable.tech/sdp/opensearch:{{ test_scenario['values']['opensearch'].split(',')[0] }}-stackable{{ test_scenario['values']['release'] }} -{% endif %} - command: - - /bin/bash - - -euxo - - pipefail - - -c - args: - - | - bin/opensearch-keystore create - - for i in keystore-secrets/*; do - key=$(basename $i) - bin/opensearch-keystore add-file "$key" "$i" - done - - cp --archive config/opensearch.keystore initialized-keystore - volumeMounts: - - name: keystore - mountPath: /stackable/opensearch/initialized-keystore - readOnly: false - - name: keystore-secrets - mountPath: /stackable/opensearch/keystore-secrets - readOnly: true - name: init-system-keystore {% if test_scenario['values']['opensearch'].find(",") > 0 %} image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" @@ -139,18 +120,7 @@ spec: - name: tls-concatenated mountPath: /stackable/opensearch/config/tls readOnly: true - - name: keystore - mountPath: /stackable/opensearch/config/opensearch.keystore - subPath: opensearch.keystore - readOnly: true volumes: - - name: keystore - emptyDir: - sizeLimit: 1Mi - - name: keystore-secrets - secret: - secretName: s3-credentials - defaultMode: 0o660 - name: s3-ca-crt secret: secretName: minio-ca-crt