Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Support hot-reloading of security configuration files ([#130]).

### Changed

- Document Helm deployed RBAC permissions and remove unnecessary permissions ([#129]).

[#129]: https://github.com/stackabletech/opensearch-operator/pull/129
[#130]: https://github.com/stackabletech/opensearch-operator/pull/130

## [26.3.0] - 2026-03-16

Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions docs/modules/opensearch/pages/usage-guide/monitoring.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ To make the metrics accessible for all users, especially Prometheus, anonymous a
----
---
apiVersion: v1
kind: Secret
kind: ConfigMap
metadata:
name: opensearch-security-config
stringData:
name: custom-opensearch-security-config
data:
config.yml: |
---
_meta:
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/opensearch/pages/usage-guide/security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ spec:

If this role group is not defined, it will be created by the operator.

Settings managed by the operator are hot-reloaded when changed, i.e. without pod restarts.

== TLS

TLS is also managed by the OpenSearch security plugin, therefore TLS is only available if the security plugin was not disabled.
Expand Down
8 changes: 8 additions & 0 deletions docs/modules/opensearch/pages/usage-guide/upgrade.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
= SDP upgrade notes
:description: Instructions for upgrading the SDP versions.

== Upgrade from SDP 26.3 to 26.7

=== Dedicated ConfigMap for security settings

The security settings defined in the cluster specification are now stored in a separate ConfigMap named `<cluster-name>-security-config`.
If you used this name for your custom security configuration, then you must rename it.
Otherwise the operator will override it.

== Upgrade from SDP 25.11 to 26.3

When upgrading the OpenSearch operator from SDP 25.11 to 26.3, you may encounter several warnings and errors in the operator logs.
Expand Down
16 changes: 12 additions & 4 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou
listeners.push(role_group_builder.build_listener());
}

if let Some(discovery_config_map) = role_builder.build_discovery_config_map() {
if let Some(discovery_config_map) = role_builder.build_maybe_discovery_config_map() {
config_maps.push(discovery_config_map);
}
if let Some(security_config_map) = role_builder.build_maybe_security_config_map() {
config_maps.push(security_config_map);
}
services.push(role_builder.build_seed_nodes_service());
listeners.push(role_builder.build_discovery_service_listener());

Expand Down Expand Up @@ -90,7 +93,7 @@ mod tests {
role_utils::GenericProductSpecificCommonConfig,
types::{
common::Port,
kubernetes::{Hostname, ListenerClassName, NamespaceName},
kubernetes::{Hostname, ListenerClassName, NamespaceName, SecretClassName},
operator::{
ClusterName, ControllerName, OperatorName, ProductName, ProductVersion,
RoleGroupName,
Expand Down Expand Up @@ -134,7 +137,8 @@ mod tests {
"my-opensearch",
"my-opensearch-nodes-cluster-manager",
"my-opensearch-nodes-coordinating",
"my-opensearch-nodes-data"
"my-opensearch-nodes-data",
"my-opensearch-security-config"
],
extract_resource_names(&resources.config_maps)
);
Expand Down Expand Up @@ -209,7 +213,11 @@ mod tests {
),
]
.into(),
ValidatedSecurity::Disabled,
ValidatedSecurity::ManagedByApi {
settings: v1alpha1::SecuritySettings::default(),
tls_server_secret_class: None,
tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"),
},
vec![],
Some(ValidatedDiscoveryEndpoint {
hostname: Hostname::from_str_unsafe("1.2.3.4"),
Expand Down
112 changes: 106 additions & 6 deletions rust/operator-binary/src/controller/build/role_builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Builder for role resources

use std::str::FromStr;
use std::{collections::BTreeMap, str::FromStr};

use stackable_operator::{
builder::meta::ObjectMetaBuilder,
Expand All @@ -23,8 +23,9 @@ use stackable_operator::{
use crate::{
controller::{
ContextNames, HTTP_PORT, HTTP_PORT_NAME, TRANSPORT_PORT, TRANSPORT_PORT_NAME,
ValidatedCluster, build::role_group_builder::RoleGroupBuilder,
ValidatedCluster, ValidatedSecurity, build::role_group_builder::RoleGroupBuilder,
},
crd::v1alpha1,
framework::{
NameIsValidLabelValue,
builder::{
Expand Down Expand Up @@ -166,7 +167,7 @@ impl<'a> RoleBuilder<'a> {
/// The discovery endpoint is derived from the status of the discovery service Listener. If the
/// status is not set yet, the reconciliation process will occur again once the Listener status
/// is updated, leading to the eventual creation of the discovery ConfigMap.
pub fn build_discovery_config_map(&self) -> Option<ConfigMap> {
pub fn build_maybe_discovery_config_map(&self) -> Option<ConfigMap> {
let discovery_endpoint = self.cluster.discovery_endpoint.as_ref()?;

let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name));
Expand Down Expand Up @@ -204,6 +205,40 @@ impl<'a> RoleBuilder<'a> {
})
}

/// Builds the [`ConfigMap`] containing the security configuration files that were defined by
/// value.
///
/// Returns `None` if the security plugin is disabled or all configuration files are
/// references.
pub fn build_maybe_security_config_map(&self) -> Option<ConfigMap> {
let metadata = self.common_metadata(security_config_map_name(&self.cluster.name));

let mut data = BTreeMap::new();

if let ValidatedSecurity::ManagedByApi { settings, .. }
| ValidatedSecurity::ManagedByOperator { settings, .. } = &self.cluster.security
{
for file_type in settings {
if let v1alpha1::SecuritySettingsFileTypeContent::Value(
v1alpha1::SecuritySettingsFileTypeContentValue { value },
) = &file_type.content
{
data.insert(file_type.filename.to_owned(), value.to_string());
}
}
}

if data.is_empty() {
None
} else {
Some(ConfigMap {
metadata,
data: Some(data),
..ConfigMap::default()
})
}
}

/// Builds a [`PodDisruptionBudget`] used by all role-groups
pub fn build_pdb(&self) -> Option<PodDisruptionBudget> {
let pdb_config = &self.cluster.role_config.common.pod_disruption_budget;
Expand Down Expand Up @@ -297,6 +332,20 @@ fn discovery_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
ConfigMapName::from_str(cluster_name.as_ref()).expect("should be a valid ConfigMap name")
}

pub fn security_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
const SUFFIX: &str = "-security-config";

// compile-time checks
const _: () = assert!(
ClusterName::MAX_LENGTH + SUFFIX.len() <= ConfigMapName::MAX_LENGTH,
"The string `<cluster_name>-security-config` must not exceed the limit of ConfigMap names."
);
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;

ConfigMapName::from_str(&format!("{}{SUFFIX}", cluster_name.as_ref()))
.expect("should be a valid ConfigMap name")
}

pub fn discovery_service_listener_name(cluster_name: &ClusterName) -> ListenerName {
// compile-time checks
const _: () = assert!(
Expand Down Expand Up @@ -640,12 +689,13 @@ mod tests {
}

#[test]
fn test_build_discovery_config_map() {
fn test_build_maybe_discovery_config_map() {
let context_names = context_names();
let role_builder = role_builder(&context_names);

let discovery_config_map = serde_json::to_value(role_builder.build_discovery_config_map())
.expect("should be serializable");
let discovery_config_map =
serde_json::to_value(role_builder.build_maybe_discovery_config_map())
.expect("should be serializable");

assert_eq!(
json!({
Expand Down Expand Up @@ -683,6 +733,56 @@ mod tests {
);
}

#[test]
fn test_build_maybe_security_config_map() {
let context_names = context_names();
let role_builder = role_builder(&context_names);

let security_config_map =
serde_json::to_value(role_builder.build_maybe_security_config_map())
.expect("should be serializable");

assert_eq!(
json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"labels": {
"app.kubernetes.io/component": "nodes",
"app.kubernetes.io/instance": "my-opensearch-cluster",
"app.kubernetes.io/managed-by": "opensearch.stackable.tech_opensearchcluster",
"app.kubernetes.io/name": "opensearch",
"app.kubernetes.io/version": "3.4.0",
"stackable.tech/vendor": "Stackable",
},
"name": "my-opensearch-cluster-security-config",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "opensearch.stackable.tech/v1alpha1",
"controller": true,
"kind": "OpenSearchCluster",
"name": "my-opensearch-cluster",
"uid": "0b1e30e6-326e-4c1a-868d-ad6598b49e8b",
},
],
},
"data": {
"action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}",
"allowlist.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"allowlist\"},\"config\":{\"enabled\":false}}",
"audit.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"audit\"},\"config\":{\"enabled\":false}}",
"config.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"config\"},\"config\":{\"dynamic\":{\"authc\":{},\"authz\":{},\"http\":{}}}}",
"internal_users.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"internalusers\"}}",
"nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}",
"roles.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"roles\"}}",
"roles_mapping.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"rolesmapping\"}}",
"tenants.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"tenants\"}}",
},
}),
security_config_map
);
}

#[test]
fn test_build_pdb() {
let context_names = context_names();
Expand Down
Loading
Loading