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/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 12f3da0..c142ffe 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -29,9 +29,40 @@ 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 + 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 + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + 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..4425335 --- /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 from in 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 832e488..9e9b4f4 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, OpenSearchKeystore}, }, framework::{ ClusterName, ControllerName, HasName, HasUid, ListenerClassName, NameIsValidLabelValue, @@ -166,9 +166,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, @@ -177,6 +179,7 @@ impl ValidatedCluster { uid: impl Into, role_config: GenericRoleConfig, role_group_configs: BTreeMap, + keystores: Vec, ) -> Self { let uid = uid.into(); ValidatedCluster { @@ -193,6 +196,7 @@ impl ValidatedCluster { uid, role_config, role_group_configs, + keystores, } } @@ -378,10 +382,13 @@ mod tests { use super::{Context, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedLogging}; use crate::{ controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig}, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchKeystore, SecretKeyRef}, + }, framework::{ ClusterName, ListenerClassName, NamespaceName, OperatorName, ProductVersion, - RoleGroupName, builder::pod::container::EnvVarSet, + RoleGroupName, SecretKey, SecretName, builder::pod::container::EnvVarSet, product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, }, @@ -494,6 +501,13 @@ mod tests { ), ] .into(), + vec![OpenSearchKeystore { + key: "Keystore1".to_string(), + 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 43c3bda..9e8affe 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -77,11 +77,14 @@ mod tests { ContextNames, OpenSearchNodeResources, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchKeystore, SecretKeyRef}, + }, framework::{ ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, - ProductName, ProductVersion, RoleGroupName, builder::pod::container::EnvVarSet, - role_utils::GenericProductSpecificCommonConfig, + ProductName, ProductVersion, RoleGroupName, SecretKey, SecretName, + builder::pod::container::EnvVarSet, role_utils::GenericProductSpecificCommonConfig, }, }; @@ -191,6 +194,13 @@ mod tests { ), ] .into(), + vec![OpenSearchKeystore { + key: "Keystore1".to_string(), + 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/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 9249042..29874f5 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)] @@ -383,6 +402,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 454d7d5..b7ba039 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -305,6 +305,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 896c344..6e8cd15 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::{ @@ -46,7 +48,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}, }, }, @@ -74,7 +76,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> { @@ -248,6 +254,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 @@ -257,7 +268,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 { @@ -288,6 +299,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, @@ -309,6 +348,7 @@ impl<'a> RoleGroupBuilder<'a> { .into_iter() .flatten() .collect(), + init_containers: Some(init_containers), node_selector: self .role_group_config .config @@ -368,6 +408,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 @@ -395,19 +487,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(), @@ -441,6 +524,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![ @@ -661,11 +754,14 @@ mod tests { ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, }, - crd::{NodeRoles, v1alpha1}, + crd::{ + NodeRoles, + v1alpha1::{self, OpenSearchKeystore, SecretKeyRef}, + }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, - OperatorName, ProductName, ProductVersion, RoleGroupName, ServiceAccountName, - ServiceName, builder::pod::container::EnvVarSet, + OperatorName, ProductName, ProductVersion, RoleGroupName, SecretKey, SecretName, + ServiceAccountName, ServiceName, builder::pod::container::EnvVarSet, product_logging::framework::VectorContainerLogConfig, role_utils::GenericProductSpecificCommonConfig, }, @@ -745,6 +841,13 @@ mod tests { role_group_config.clone(), )] .into(), + vec![OpenSearchKeystore { + key: "Keystore1".to_string(), + secret_key_ref: SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file"), + }, + }], ) } @@ -1019,6 +1122,12 @@ mod tests { { "mountPath": "/stackable/log", "name": "log" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch.keystore", + "name": "keystore", + "readOnly": true, + "subPath": "opensearch.keystore", } ] }, @@ -1120,6 +1229,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 }, @@ -1145,7 +1291,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 c23ff48..443eeb7 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -140,6 +140,7 @@ pub fn validate( uid, cluster.spec.nodes.role_config.clone(), role_group_configs, + cluster.spec.cluster_config.keystore.clone(), )) } @@ -274,11 +275,11 @@ mod tests { controller::{ContextNames, ValidatedCluster, ValidatedLogging, ValidatedOpenSearchConfig}, crd::{ NodeRoles, - v1alpha1::{self}, + v1alpha1::{self, OpenSearchKeystore, SecretKeyRef}, }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, - OperatorName, ProductName, ProductVersion, RoleGroupName, + OperatorName, ProductName, ProductVersion, RoleGroupName, SecretKey, SecretName, builder::pod::container::{EnvVarName, EnvVarSet}, product_logging::framework::{ ValidatedContainerLogConfigChoice, VectorContainerLogConfig, @@ -492,6 +493,13 @@ mod tests { } )] .into(), + vec![OpenSearchKeystore { + key: "Keystore1".to_string(), + secret_key_ref: SecretKeyRef { + name: SecretName::from_str_unsafe("my-keystore-secret"), + key: SecretKey::from_str_unsafe("my-keystore-file") + } + }] )), result.ok() ); @@ -669,6 +677,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![OpenSearchKeystore { + key: "Keystore1".to_string(), + secret_key_ref: 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 4803e70..4e7b10d 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -30,7 +30,8 @@ use crate::{ constant, framework::{ ClusterName, ConfigMapName, ContainerName, ListenerClassName, NameIsValidLabelValue, - ProductName, RoleName, role_utils::GenericProductSpecificCommonConfig, + ProductName, RoleName, SecretKey, SecretName, + role_utils::GenericProductSpecificCommonConfig, }, }; @@ -80,6 +81,9 @@ 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) @@ -88,6 +92,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: String, + + /// 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. // @@ -191,6 +213,9 @@ pub mod versioned { #[serde(rename = "vector")] Vector, + + #[serde(rename = "init-keystore")] + InitKeystore, } #[derive(Clone, Debug, Default, JsonSchema, PartialEq, Fragment)] @@ -323,6 +348,7 @@ 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") } diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 3442b2e..59947b0 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -338,7 +338,7 @@ attributed_string_type! { } attributed_string_type! { ConfigMapKey, - "The key for a ConfigMap or Secret", + "The key for a ConfigMap", "log4j2.properties", // see https://github.com/kubernetes/kubernetes/blob/v1.34.1/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L435-L451 (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), @@ -402,6 +402,21 @@ attributed_string_type! { (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), is_rfc_1123_dns_subdomain_name } +attributed_string_type! { + SecretName, + "The name of a Secret", + "opensearch-security-config", + (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), + is_rfc_1123_dns_subdomain_name +} +attributed_string_type! { + SecretKey, + "The key for a Secret", + "opensearch.yml", + // see https://github.com/kubernetes/kubernetes/blob/v1.34.1/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L435-L451 + (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), + is_config_map_key +} attributed_string_type! { ServiceAccountName, "The name of a ServiceAccount", diff --git a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 index 2839ecb..da59f2f 100644 --- a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.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: s3.client.default.access_key + - key: s3.client.default.secret_key + secretKeyRef: + name: s3-credentials + key: s3.client.default.secret_key +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -48,34 +57,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] }}" @@ -109,18 +90,7 @@ spec: - name: tls 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