Skip to content

Commit 28b1f27

Browse files
feat: Support hot-reloading for security configuration files
1 parent 7952343 commit 28b1f27

9 files changed

Lines changed: 385 additions & 142 deletions

File tree

rust/operator-binary/src/controller/build.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou
3333
listeners.push(role_group_builder.build_listener());
3434
}
3535

36-
if let Some(discovery_config_map) = role_builder.build_discovery_config_map() {
36+
if let Some(discovery_config_map) = role_builder.build_maybe_discovery_config_map() {
3737
config_maps.push(discovery_config_map);
3838
}
39+
if let Some(security_config_map) = role_builder.build_maybe_security_config_map() {
40+
config_maps.push(security_config_map);
41+
}
3942
services.push(role_builder.build_seed_nodes_service());
4043
listeners.push(role_builder.build_discovery_service_listener());
4144

rust/operator-binary/src/controller/build/role_builder.rs

Lines changed: 136 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Builder for role resources
22
3-
use std::str::FromStr;
3+
use std::{collections::BTreeMap, str::FromStr};
44

55
use stackable_operator::{
66
builder::meta::ObjectMetaBuilder,
@@ -13,7 +13,6 @@ use stackable_operator::{
1313
rbac::v1::{ClusterRole, RoleBinding, RoleRef, Subject},
1414
},
1515
},
16-
kube::api::ObjectMeta,
1716
kvp::{
1817
Label, Labels,
1918
consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE},
@@ -23,12 +22,14 @@ use stackable_operator::{
2322
use crate::{
2423
controller::{
2524
ContextNames, HTTP_PORT, HTTP_PORT_NAME, TRANSPORT_PORT, TRANSPORT_PORT_NAME,
26-
ValidatedCluster, build::role_group_builder::RoleGroupBuilder,
25+
ValidatedCluster, ValidatedSecurity, build::role_group_builder::RoleGroupBuilder,
2726
},
27+
crd::v1alpha1,
2828
framework::{
2929
NameIsValidLabelValue,
3030
builder::{
31-
meta::ownerreference_from_resource, pdb::pod_disruption_budget_builder_with_role,
31+
meta::{annotation_ignore_restarter, ownerreference_from_resource},
32+
pdb::pod_disruption_budget_builder_with_role,
3233
},
3334
role_utils::ResourceNames,
3435
types::{
@@ -80,7 +81,9 @@ impl<'a> RoleBuilder<'a> {
8081

8182
/// Builds a ServiceAccount used by all role-groups
8283
pub fn build_service_account(&self) -> ServiceAccount {
83-
let metadata = self.common_metadata(self.resource_names.service_account_name());
84+
let metadata = self
85+
.common_metadata(self.resource_names.service_account_name())
86+
.build();
8487

8588
ServiceAccount {
8689
metadata,
@@ -90,7 +93,9 @@ impl<'a> RoleBuilder<'a> {
9093

9194
/// Builds a RoleBinding used by all role-groups
9295
pub fn build_role_binding(&self) -> RoleBinding {
93-
let metadata = self.common_metadata(self.resource_names.role_binding_name());
96+
let metadata = self
97+
.common_metadata(self.resource_names.role_binding_name())
98+
.build();
9499

95100
RoleBinding {
96101
metadata,
@@ -116,7 +121,9 @@ impl<'a> RoleBuilder<'a> {
116121
..ServicePort::default()
117122
}];
118123

119-
let metadata = self.common_metadata(seed_nodes_service_name(&self.cluster.name));
124+
let metadata = self
125+
.common_metadata(seed_nodes_service_name(&self.cluster.name))
126+
.build();
120127

121128
let service_selector =
122129
RoleGroupBuilder::cluster_manager_labels(&self.cluster, self.context_names);
@@ -140,7 +147,9 @@ impl<'a> RoleBuilder<'a> {
140147

141148
/// Builds a Listener whose status is used to populate the discovery ConfigMap.
142149
pub fn build_discovery_service_listener(&self) -> listener::v1alpha1::Listener {
143-
let metadata = self.common_metadata(discovery_service_listener_name(&self.cluster.name));
150+
let metadata = self
151+
.common_metadata(discovery_service_listener_name(&self.cluster.name))
152+
.build();
144153

145154
let listener_class = &self.cluster.role_config.discovery_service_listener_class;
146155

@@ -166,10 +175,12 @@ impl<'a> RoleBuilder<'a> {
166175
/// The discovery endpoint is derived from the status of the discovery service Listener. If the
167176
/// status is not set yet, the reconciliation process will occur again once the Listener status
168177
/// is updated, leading to the eventual creation of the discovery ConfigMap.
169-
pub fn build_discovery_config_map(&self) -> Option<ConfigMap> {
178+
pub fn build_maybe_discovery_config_map(&self) -> Option<ConfigMap> {
170179
let discovery_endpoint = self.cluster.discovery_endpoint.as_ref()?;
171180

172-
let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name));
181+
let metadata = self
182+
.common_metadata(discovery_config_map_name(&self.cluster.name))
183+
.build();
173184

174185
let protocol = if self.cluster.is_server_tls_enabled() {
175186
"https"
@@ -204,6 +215,43 @@ impl<'a> RoleBuilder<'a> {
204215
})
205216
}
206217

218+
/// Builds the [`ConfigMap`] containing the security configuration files that were defined by
219+
/// value.
220+
///
221+
/// Returns `None` if the security plugin is disabled or all configuration files are
222+
/// references.
223+
pub fn build_maybe_security_config_map(&self) -> Option<ConfigMap> {
224+
let metadata = self
225+
.common_metadata(security_config_map_name(&self.cluster.name))
226+
.with_annotation(annotation_ignore_restarter())
227+
.build();
228+
229+
let mut data = BTreeMap::new();
230+
231+
if let ValidatedSecurity::ManagedByApi { settings, .. }
232+
| ValidatedSecurity::ManagedByOperator { settings, .. } = &self.cluster.security
233+
{
234+
for file_type in settings {
235+
if let v1alpha1::SecuritySettingsFileTypeContent::Value(
236+
v1alpha1::SecuritySettingsFileTypeContentValue { value },
237+
) = &file_type.content
238+
{
239+
data.insert(file_type.filename.to_owned(), value.to_string());
240+
}
241+
}
242+
}
243+
244+
if data.is_empty() {
245+
None
246+
} else {
247+
Some(ConfigMap {
248+
metadata,
249+
data: Some(data),
250+
..ConfigMap::default()
251+
})
252+
}
253+
}
254+
207255
/// Builds a [`PodDisruptionBudget`] used by all role-groups
208256
pub fn build_pdb(&self) -> Option<PodDisruptionBudget> {
209257
let pdb_config = &self.cluster.role_config.common.pod_disruption_budget;
@@ -229,17 +277,20 @@ impl<'a> RoleBuilder<'a> {
229277
}
230278

231279
/// Common metadata for role resources
232-
fn common_metadata(&self, resource_name: impl Into<String>) -> ObjectMeta {
233-
ObjectMetaBuilder::new()
280+
fn common_metadata(&self, resource_name: impl Into<String>) -> ObjectMetaBuilder {
281+
let mut builder = ObjectMetaBuilder::new();
282+
283+
builder
234284
.name(resource_name)
235285
.namespace(&self.cluster.namespace)
236286
.ownerreference(ownerreference_from_resource(
237287
&self.cluster,
238288
None,
239289
Some(true),
240290
))
241-
.with_labels(self.labels())
242-
.build()
291+
.with_labels(self.labels());
292+
293+
builder
243294
}
244295

245296
/// Common labels for role resources
@@ -297,6 +348,20 @@ fn discovery_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
297348
ConfigMapName::from_str(cluster_name.as_ref()).expect("should be a valid ConfigMap name")
298349
}
299350

351+
pub fn security_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
352+
const SUFFIX: &str = "-security-config";
353+
354+
// compile-time checks
355+
const _: () = assert!(
356+
ClusterName::MAX_LENGTH + SUFFIX.len() <= ConfigMapName::MAX_LENGTH,
357+
"The string `<cluster_name>-security-config` must not exceed the limit of ConfigMap names."
358+
);
359+
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;
360+
361+
ConfigMapName::from_str(&format!("{}{SUFFIX}", cluster_name.as_ref()))
362+
.expect("should be a valid ConfigMap name")
363+
}
364+
300365
pub fn discovery_service_listener_name(cluster_name: &ClusterName) -> ListenerName {
301366
// compile-time checks
302367
const _: () = assert!(
@@ -640,12 +705,13 @@ mod tests {
640705
}
641706

642707
#[test]
643-
fn test_build_discovery_config_map() {
708+
fn test_build_maybe_discovery_config_map() {
644709
let context_names = context_names();
645710
let role_builder = role_builder(&context_names);
646711

647-
let discovery_config_map = serde_json::to_value(role_builder.build_discovery_config_map())
648-
.expect("should be serializable");
712+
let discovery_config_map =
713+
serde_json::to_value(role_builder.build_maybe_discovery_config_map())
714+
.expect("should be serializable");
649715

650716
assert_eq!(
651717
json!({
@@ -683,6 +749,59 @@ mod tests {
683749
);
684750
}
685751

752+
#[test]
753+
fn test_build_maybe_security_config_map() {
754+
let context_names = context_names();
755+
let role_builder = role_builder(&context_names);
756+
757+
let security_config_map =
758+
serde_json::to_value(role_builder.build_maybe_security_config_map())
759+
.expect("should be serializable");
760+
761+
assert_eq!(
762+
json!({
763+
"apiVersion": "v1",
764+
"kind": "ConfigMap",
765+
"metadata": {
766+
"annotations": {
767+
"restarter.stackable.tech/ignore": "true"
768+
},
769+
"labels": {
770+
"app.kubernetes.io/component": "nodes",
771+
"app.kubernetes.io/instance": "my-opensearch-cluster",
772+
"app.kubernetes.io/managed-by": "opensearch.stackable.tech_opensearchcluster",
773+
"app.kubernetes.io/name": "opensearch",
774+
"app.kubernetes.io/version": "3.4.0",
775+
"stackable.tech/vendor": "Stackable",
776+
},
777+
"name": "my-opensearch-cluster-security-config",
778+
"namespace": "default",
779+
"ownerReferences": [
780+
{
781+
"apiVersion": "opensearch.stackable.tech/v1alpha1",
782+
"controller": true,
783+
"kind": "OpenSearchCluster",
784+
"name": "my-opensearch-cluster",
785+
"uid": "0b1e30e6-326e-4c1a-868d-ad6598b49e8b",
786+
},
787+
],
788+
},
789+
"data": {
790+
"action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}",
791+
"allowlist.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"allowlist\"},\"config\":{\"enabled\":false}}",
792+
"audit.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"audit\"},\"config\":{\"enabled\":false}}",
793+
"config.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"config\"},\"config\":{\"dynamic\":{\"authc\":{},\"authz\":{},\"http\":{}}}}",
794+
"internal_users.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"internalusers\"}}",
795+
"nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}",
796+
"roles.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"roles\"}}",
797+
"roles_mapping.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"rolesmapping\"}}",
798+
"tenants.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"tenants\"}}",
799+
},
800+
}),
801+
security_config_map
802+
);
803+
}
804+
686805
#[test]
687806
fn test_build_pdb() {
688807
let context_names = context_names();

0 commit comments

Comments
 (0)