From 28b1f2757f2072bc353d726cb216d0bb2e40cf4b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 27 Mar 2026 16:17:06 +0100 Subject: [PATCH] feat: Support hot-reloading for security configuration files --- rust/operator-binary/src/controller/build.rs | 5 +- .../src/controller/build/role_builder.rs | 153 ++++++++++++-- .../controller/build/role_group_builder.rs | 138 ++++++------- .../src/controller/build/scripts/test.sh | 14 ++ .../build/scripts/update-security-config.sh | 188 ++++++++++++++---- .../src/framework/builder/meta.rs | 21 +- .../security-config/10-security-config.yaml | 4 + .../kuttl/security-config/11-assert.yaml | 2 +- .../kuttl/security-config/21-assert.yaml | 2 +- 9 files changed, 385 insertions(+), 142 deletions(-) create mode 100755 rust/operator-binary/src/controller/build/scripts/test.sh diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 5be50f3..948b498 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -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()); diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 385e0fc..93de96d 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -1,6 +1,6 @@ //! Builder for role resources -use std::str::FromStr; +use std::{collections::BTreeMap, str::FromStr}; use stackable_operator::{ builder::meta::ObjectMetaBuilder, @@ -13,7 +13,6 @@ use stackable_operator::{ rbac::v1::{ClusterRole, RoleBinding, RoleRef, Subject}, }, }, - kube::api::ObjectMeta, kvp::{ Label, Labels, consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, @@ -23,12 +22,14 @@ 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::{ - meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role, + meta::{annotation_ignore_restarter, ownerreference_from_resource}, + pdb::pod_disruption_budget_builder_with_role, }, role_utils::ResourceNames, types::{ @@ -80,7 +81,9 @@ impl<'a> RoleBuilder<'a> { /// Builds a ServiceAccount used by all role-groups pub fn build_service_account(&self) -> ServiceAccount { - let metadata = self.common_metadata(self.resource_names.service_account_name()); + let metadata = self + .common_metadata(self.resource_names.service_account_name()) + .build(); ServiceAccount { metadata, @@ -90,7 +93,9 @@ impl<'a> RoleBuilder<'a> { /// Builds a RoleBinding used by all role-groups pub fn build_role_binding(&self) -> RoleBinding { - let metadata = self.common_metadata(self.resource_names.role_binding_name()); + let metadata = self + .common_metadata(self.resource_names.role_binding_name()) + .build(); RoleBinding { metadata, @@ -116,7 +121,9 @@ impl<'a> RoleBuilder<'a> { ..ServicePort::default() }]; - let metadata = self.common_metadata(seed_nodes_service_name(&self.cluster.name)); + let metadata = self + .common_metadata(seed_nodes_service_name(&self.cluster.name)) + .build(); let service_selector = RoleGroupBuilder::cluster_manager_labels(&self.cluster, self.context_names); @@ -140,7 +147,9 @@ impl<'a> RoleBuilder<'a> { /// Builds a Listener whose status is used to populate the discovery ConfigMap. pub fn build_discovery_service_listener(&self) -> listener::v1alpha1::Listener { - let metadata = self.common_metadata(discovery_service_listener_name(&self.cluster.name)); + let metadata = self + .common_metadata(discovery_service_listener_name(&self.cluster.name)) + .build(); let listener_class = &self.cluster.role_config.discovery_service_listener_class; @@ -166,10 +175,12 @@ 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 { + pub fn build_maybe_discovery_config_map(&self) -> Option { let discovery_endpoint = self.cluster.discovery_endpoint.as_ref()?; - let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name)); + let metadata = self + .common_metadata(discovery_config_map_name(&self.cluster.name)) + .build(); let protocol = if self.cluster.is_server_tls_enabled() { "https" @@ -204,6 +215,43 @@ 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 { + let metadata = self + .common_metadata(security_config_map_name(&self.cluster.name)) + .with_annotation(annotation_ignore_restarter()) + .build(); + + 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 { let pdb_config = &self.cluster.role_config.common.pod_disruption_budget; @@ -229,8 +277,10 @@ impl<'a> RoleBuilder<'a> { } /// Common metadata for role resources - fn common_metadata(&self, resource_name: impl Into) -> ObjectMeta { - ObjectMetaBuilder::new() + fn common_metadata(&self, resource_name: impl Into) -> ObjectMetaBuilder { + let mut builder = ObjectMetaBuilder::new(); + + builder .name(resource_name) .namespace(&self.cluster.namespace) .ownerreference(ownerreference_from_resource( @@ -238,8 +288,9 @@ impl<'a> RoleBuilder<'a> { None, Some(true), )) - .with_labels(self.labels()) - .build() + .with_labels(self.labels()); + + builder } /// Common labels for role resources @@ -297,6 +348,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 `-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!( @@ -640,12 +705,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!({ @@ -683,6 +749,59 @@ 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": { + "annotations": { + "restarter.stackable.tech/ignore": "true" + }, + "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(); 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 975efcd..4c5a81c 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -47,8 +47,11 @@ use crate::{ controller::{ ContextNames, HTTP_PORT, HTTP_PORT_NAME, OpenSearchRoleGroupConfig, TRANSPORT_PORT, TRANSPORT_PORT_NAME, ValidatedCluster, ValidatedNodeRole, ValidatedSecurity, - build::product_logging::config::{ - MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, vector_config_file_extra_env_vars, + build::{ + product_logging::config::{ + MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, vector_config_file_extra_env_vars, + }, + role_builder::security_config_map_name, }, }, crd::{ExtendedSecuritySettingsFileType, v1alpha1}, @@ -294,19 +297,6 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if let RoleGroupSecurityMode::Initializing { settings, .. } - | RoleGroupSecurityMode::Managing { settings, .. } = &self.security_mode - { - 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()); - } - } - } - ConfigMap { metadata, data: Some(data), @@ -725,7 +715,7 @@ impl<'a> RoleGroupBuilder<'a> { }; if let RoleGroupSecurityMode::Initializing { settings, .. } = &self.security_mode { - volume_mounts.extend(self.security_config_volume_mounts(settings)); + volume_mounts.extend(self.security_config_volume_mounts(settings, true)); }; if !self.cluster.keystores.is_empty() { @@ -791,20 +781,35 @@ impl<'a> RoleGroupBuilder<'a> { fn security_config_volume_mounts( &self, settings: &v1alpha1::SecuritySettings, + use_sub_path: bool, ) -> Vec { let mut volume_mounts = vec![]; let opensearch_path_conf = self.node_config.opensearch_path_conf(); for file_type in settings { - volume_mounts.push(VolumeMount { - mount_path: format!( + let mount_path; + let sub_path; + + if use_sub_path { + mount_path = format!( "{opensearch_path_conf}/opensearch-security/{filename}", filename = file_type.filename.to_owned() - ), + ); + sub_path = Some(file_type.filename.to_owned()); + } else { + mount_path = format!( + "{opensearch_path_conf}/opensearch-security/{file_type}", + file_type = file_type.id + ); + sub_path = None; + } + + volume_mounts.push(VolumeMount { + mount_path, name: Self::security_settings_file_type_volume_name(&file_type).to_string(), read_only: Some(true), - sub_path: Some(file_type.filename.to_owned()), + sub_path, ..VolumeMount::default() }); } @@ -877,7 +882,7 @@ impl<'a> RoleGroupBuilder<'a> { ..VolumeMount::default() }, ]; - volume_mounts.extend(self.security_config_volume_mounts(settings)); + volume_mounts.extend(self.security_config_volume_mounts(settings, false)); let mut env_vars = EnvVarSet::new() .with_value( @@ -1125,7 +1130,7 @@ impl<'a> RoleGroupBuilder<'a> { mode: Some(0o660), path: file_type.filename.to_owned(), }]), - name: self.resource_names.role_group_config_map().to_string(), + name: security_config_map_name(&self.cluster.name).to_string(), ..Default::default() }), ..Volume::default() @@ -1628,12 +1633,8 @@ mod tests { } #[rstest] - #[case::security_mode_initializing(TestSecurityMode::Initializing)] - #[case::security_mode_managing(TestSecurityMode::Managing)] - #[case::security_mode_participating(TestSecurityMode::Participating)] - #[case::security_mode_disabled(TestSecurityMode::Disabled)] - fn test_build_config_map(#[case] security_mode: TestSecurityMode) { - let cluster = validated_cluster(security_mode); + fn test_build_config_map() { + let cluster = validated_cluster(TestSecurityMode::Disabled); let context_names = context_names(); let role_group_builder = role_group_builder(&cluster, &context_names); @@ -1649,26 +1650,6 @@ mod tests { // vector.yaml is a static file and does not have to be repeated here. config_map["data"]["vector.yaml"].take(); - let expected_data = match security_mode { - TestSecurityMode::Initializing | TestSecurityMode::Managing => json!({ - "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\":{}}}}", - "log4j2.properties": null, - "nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}", - "opensearch.yml": null, - "roles_mapping.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"rolesmapping\"}}", - "tenants.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"tenants\"}}", - "vector.yaml": null - }), - TestSecurityMode::Participating | TestSecurityMode::Disabled => json!({ - "log4j2.properties": null, - "opensearch.yml": null, - "vector.yaml": null - }), - }; - assert_eq!( json!({ "apiVersion": "v1", @@ -1695,7 +1676,11 @@ mod tests { } ] }, - "data": expected_data + "data": { + "log4j2.properties": null, + "opensearch.yml": null, + "vector.yaml": null + } }), config_map ); @@ -2297,58 +2282,49 @@ mod tests { "name": "log", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/action_groups.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/actiongroups", "name": "security-config-file-actiongroups", "readOnly": true, - "subPath": "action_groups.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/allowlist.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/allowlist", "name": "security-config-file-allowlist", "readOnly": true, - "subPath": "allowlist.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/audit", "name": "security-config-file-audit", "readOnly": true, - "subPath": "audit.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/config.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/config", "name": "security-config-file-config", "readOnly": true, - "subPath": "config.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/internal_users.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/internalusers", "name": "security-config-file-internalusers", "readOnly": true, - "subPath": "internal_users.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/nodes_dn.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/nodesdn", "name": "security-config-file-nodesdn", "readOnly": true, - "subPath": "nodes_dn.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/roles.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/roles", "name": "security-config-file-roles", "readOnly": true, - "subPath": "roles.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/roles_mapping.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/rolesmapping", "name": "security-config-file-rolesmapping", "readOnly": true, - "subPath": "roles_mapping.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/tenants.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/tenants", "name": "security-config-file-tenants", "readOnly": true, - "subPath": "tenants.yml", }, ], }); @@ -2546,7 +2522,7 @@ mod tests { "path": "action_groups.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-actiongroups" }, @@ -2559,7 +2535,7 @@ mod tests { "path": "allowlist.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-allowlist" }, @@ -2572,7 +2548,7 @@ mod tests { "path": "audit.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-audit" }, @@ -2585,7 +2561,7 @@ mod tests { "path": "config.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-config" }, @@ -2611,7 +2587,7 @@ mod tests { "path": "nodes_dn.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-nodesdn" }, @@ -2637,7 +2613,7 @@ mod tests { "path": "roles_mapping.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-rolesmapping" }, @@ -2650,7 +2626,7 @@ mod tests { "path": "tenants.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-tenants" }, @@ -2756,7 +2732,7 @@ mod tests { "path": "action_groups.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-actiongroups" }, @@ -2769,7 +2745,7 @@ mod tests { "path": "allowlist.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-allowlist" }, @@ -2782,7 +2758,7 @@ mod tests { "path": "audit.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-audit" }, @@ -2795,7 +2771,7 @@ mod tests { "path": "config.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-config" }, @@ -2821,7 +2797,7 @@ mod tests { "path": "nodes_dn.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-nodesdn" }, @@ -2847,7 +2823,7 @@ mod tests { "path": "roles_mapping.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-rolesmapping" }, @@ -2860,7 +2836,7 @@ mod tests { "path": "tenants.yml" } ], - "name": "my-opensearch-cluster-nodes-default" + "name": "my-opensearch-cluster-security-config" }, "name": "security-config-file-tenants" }, diff --git a/rust/operator-binary/src/controller/build/scripts/test.sh b/rust/operator-binary/src/controller/build/scripts/test.sh new file mode 100755 index 0000000..a2cd9ca --- /dev/null +++ b/rust/operator-binary/src/controller/build/scripts/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +OPENSEARCH_PATH_CONF="$(pwd)/test-config" \ + POD_NAME="security-config-0" \ + MANAGE_ACTIONGROUPS="true" \ + MANAGE_ALLOWLIST="true" \ + MANAGE_AUDIT="false" \ + MANAGE_CONFIG="true" \ + MANAGE_INTERNALUSERS="true" \ + MANAGE_NODESDN="false" \ + MANAGE_ROLES="true" \ + MANAGE_ROLESMAPPING="true" \ + MANAGE_TENANTS="true" \ + sh ./update-security-config.sh diff --git a/rust/operator-binary/src/controller/build/scripts/update-security-config.sh b/rust/operator-binary/src/controller/build/scripts/update-security-config.sh index d84a550..9afeb5b 100644 --- a/rust/operator-binary/src/controller/build/scripts/update-security-config.sh +++ b/rust/operator-binary/src/controller/build/scripts/update-security-config.sh @@ -1,7 +1,53 @@ #!/usr/bin/env bash +# +# Expected environment variables: +# - OPENSEARCH_PATH_CONF +# - POD_NAME +# - MANAGE_ACTIONGROUPS +# - MANAGE_ALLOWLIST +# - MANAGE_AUDIT +# - MANAGE_CONFIG +# - MANAGE_INTERNALUSERS +# - MANAGE_NODESDN +# - MANAGE_ROLES +# - MANAGE_ROLESMAPPING +# - MANAGE_TENANTS + +# TODO config_files vs. configuration_files set -u -o pipefail +VECTOR_CONTROL_DIR=/stackable/log/_vector +SECURITY_CONFIG_DIR="$OPENSEARCH_PATH_CONF/opensearch-security" + +declare -a CONFIG_FILETYPES=( + actiongroups + allowlist + audit + config + internalusers + nodesdn + roles + rolesmapping + tenants +) + +declare -A CONFIG_FILENAME=( + [actiongroups]=action_groups.yml + [allowlist]=allowlist.yml + [audit]=audit.yml + [config]=config.yml + [internalusers]=internal_users.yml + [nodesdn]=nodes_dn.yml + [roles]=roles.yml + [rolesmapping]=roles_mapping.yml + [tenants]=tenants.yml +) + +declare -a managed_filetypes + +last_applied_config_hashes="" + function log () { level="$1" message="$2" @@ -10,6 +56,12 @@ function log () { echo "$timestamp [$level] $message" } +function debug () { + message="$*" + + log DEBUG "$message" +} + function info () { message="$*" @@ -22,33 +74,93 @@ function warn () { log WARN "$message" } -function wait_seconds () { +function config_file () { + filetype="$1" + + echo "$SECURITY_CONFIG_DIR/${CONFIG_FILENAME[$filetype]}" +} + +function symlink_config_files () { + for filetype in "${CONFIG_FILETYPES[@]}" + do + ln --force --symbolic \ + "$SECURITY_CONFIG_DIR/$filetype/${CONFIG_FILENAME[$filetype]}" \ + "$(config_file $filetype)" + done +} + +function initialize_managed_configuration_filetypes () { + for filetype in "${CONFIG_FILETYPES[@]}" + do + envvar="MANAGE_${filetype^^}" + if test "${!envvar}" = "true" + then + info "Watch managed configuration type \"$filetype\"." + + managed_filetypes+=("$filetype") + else + info "Skip unmanaged configuration type \"$filetype\"." + fi + done +} + +function calculate_config_hashes () { + for filetype in "${managed_filetypes[@]}" + do + file=$(config_file "$filetype") + sha256sum "$file" + done +} + +function wait_seconds_or_shutdown () { seconds="$1" - if test "$seconds" = 0 - then - info "Wait until pod is restarted..." - else - info "Wait for $seconds seconds..." - fi + debug "Wait for $seconds seconds..." - if test ! -e /stackable/log/_vector/shutdown + if test ! -e "$VECTOR_CONTROL_DIR/shutdown" then - mkdir --parents /stackable/log/_vector inotifywait \ --quiet --quiet \ --timeout "$seconds" \ --event create \ - /stackable/log/_vector + "$VECTOR_CONTROL_DIR" fi - if test -e /stackable/log/_vector/shutdown + # Only the file named "shutdown" should be created in + # VECTOR_CONTROL_DIR. If another file is created instead, this + # function will return early; this is acceptable and has no adverse + # effects. + if test -e "$VECTOR_CONTROL_DIR/shutdown" then info "Shut down" exit 0 fi } +function wait_for_configuration_changes_or_shutdown () { + info "Wait for security configuration changes..." + + while test "$(calculate_config_hashes)" = "$last_applied_config_hashes" + do + wait_seconds_or_shutdown 10 + done + + info "Configuration change detected" +} + +function wait_for_shutdown () { + until test ! -e "$VECTOR_CONTROL_DIR/shutdown" + do + inotifywait \ + --quiet --quiet \ + --event create \ + "$VECTOR_CONTROL_DIR" + done + + info "Shut down" + exit 0 +} + function check_pod () { POD_INDEX="${POD_NAME##*-}" @@ -62,34 +174,34 @@ function check_pod () { "configuration. The security configuration is managed by" \ "the pod \"$MANAGING_POD\"." - wait_seconds 0 + wait_for_shutdown fi } function initialize_security_index() { info "Initialize the security index." + last_applied_config_hashes=$(calculate_config_hashes) + until plugins/opensearch-security/tools/securityadmin.sh \ - --configdir "$OPENSEARCH_PATH_CONF/opensearch-security" \ + --configdir "$SECURITY_CONFIG_DIR" \ --disable-host-name-verification \ -cacert "$OPENSEARCH_PATH_CONF/tls/ca.crt" \ -cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ -key "$OPENSEARCH_PATH_CONF/tls/tls.key" do warn "Initializing the security index failed." - wait_seconds 10 + wait_seconds_or_shutdown 10 done } function update_config () { - filetype="$1" - filename="$2" + last_applied_config_hashes=$(calculate_config_hashes) - file="$OPENSEARCH_PATH_CONF/opensearch-security/$filename" + for filetype in "${managed_filetypes[@]}" + do + file=$(config_file "$filetype") - envvar="MANAGE_${filetype^^}" - if test "${!envvar}" = "true" - then info "Update managed configuration type \"$filetype\"." until plugins/opensearch-security/tools/securityadmin.sh \ @@ -101,11 +213,9 @@ function update_config () { -key "$OPENSEARCH_PATH_CONF/tls/tls.key" do warn "Updating \"$filetype\" in the security index failed." - wait_seconds 10 + wait_seconds_or_shutdown 10 done - else - info "Skip unmanaged configuration type \"$filetype\"." - fi + done } function update_security_index() { @@ -123,29 +233,27 @@ function update_security_index() { then info "The security index is already initialized." - update_config actiongroups action_groups.yml - update_config allowlist allowlist.yml - update_config audit audit.yml - update_config config config.yml - update_config internalusers internal_users.yml - update_config nodesdn nodes_dn.yml - update_config roles roles.yml - update_config rolesmapping roles_mapping.yml - update_config tenants tenants.yml + update_config elif test "$STATUS_CODE" = "404" then initialize_security_index else warn "Checking the security index failed." - wait_seconds 10 - check_security_index + wait_seconds_or_shutdown 10 + update_security_index fi } -check_pod +# Ensure that VECTOR_CONTROL_DIR exists, so that calls to inotifywait do not +# fail. +mkdir --parents "$VECTOR_CONTROL_DIR" -update_security_index +check_pod +symlink_config_files +initialize_managed_configuration_filetypes -info "Wait for security configuration changes..." -# Wait until the pod is restarted due to a change of the Secret. -wait_seconds 0 +while true +do + update_security_index + wait_for_configuration_changes_or_shutdown +done diff --git a/rust/operator-binary/src/framework/builder/meta.rs b/rust/operator-binary/src/framework/builder/meta.rs index 5034004..b1b8a3e 100644 --- a/rust/operator-binary/src/framework/builder/meta.rs +++ b/rust/operator-binary/src/framework/builder/meta.rs @@ -1,6 +1,7 @@ use stackable_operator::{ builder::meta::OwnerReferenceBuilder, k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference, kube::Resource, + kvp::Annotation, }; use crate::framework::{HasName, HasUid}; @@ -28,6 +29,15 @@ pub fn ownerreference_from_resource( ) } +/// Annotation which signals the restarter to ignore this resource. +pub fn annotation_ignore_restarter() -> Annotation { + Annotation::try_from(( + "restarter.stackable.tech/ignore".to_owned(), + "true".to_owned(), + )) + .expect("should be a valid annotation") +} + #[cfg(test)] mod tests { use std::borrow::Cow; @@ -37,7 +47,10 @@ mod tests { kube::Resource, }; - use crate::framework::{HasName, HasUid, Uid, builder::meta::ownerreference_from_resource}; + use crate::framework::{ + HasName, HasUid, Uid, + builder::meta::{annotation_ignore_restarter, ownerreference_from_resource}, + }; struct Cluster { object_meta: ObjectMeta, @@ -121,4 +134,10 @@ mod tests { assert_eq!(expected_owner_reference, actual_owner_reference); } + + #[test] + fn test_annotation_ignore_restarter() { + // Test that the functions do not panic + annotation_ignore_restarter(); + } } diff --git a/tests/templates/kuttl/security-config/10-security-config.yaml b/tests/templates/kuttl/security-config/10-security-config.yaml index a411ee8..91f5e63 100644 --- a/tests/templates/kuttl/security-config/10-security-config.yaml +++ b/tests/templates/kuttl/security-config/10-security-config.yaml @@ -3,6 +3,8 @@ apiVersion: v1 kind: Secret metadata: name: security-config-file-internal-users + annotations: + restarter.stackable.tech/ignore: "true" stringData: internal_users.yml: | --- @@ -21,6 +23,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: security-config + annotations: + restarter.stackable.tech/ignore: "true" data: roles.yml: | --- diff --git a/tests/templates/kuttl/security-config/11-assert.yaml b/tests/templates/kuttl/security-config/11-assert.yaml index 11166f9..9f3d565 100644 --- a/tests/templates/kuttl/security-config/11-assert.yaml +++ b/tests/templates/kuttl/security-config/11-assert.yaml @@ -30,7 +30,7 @@ status: apiVersion: v1 kind: ConfigMap metadata: - name: opensearch-nodes-security-config + name: opensearch-security-config data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' allowlist.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' diff --git a/tests/templates/kuttl/security-config/21-assert.yaml b/tests/templates/kuttl/security-config/21-assert.yaml index bffbbbc..9cab8a4 100644 --- a/tests/templates/kuttl/security-config/21-assert.yaml +++ b/tests/templates/kuttl/security-config/21-assert.yaml @@ -6,7 +6,7 @@ timeout: 120 apiVersion: v1 kind: ConfigMap metadata: - name: opensearch-nodes-security-config + name: opensearch-security-config data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' allowlist.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}'