Skip to content
Draft
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
2 changes: 2 additions & 0 deletions docs/modules/hbase/pages/reference/discovery.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The ConfigMap data values are formatted as Hadoop XML files which allows simple

`hbase-site.xml`::
Contains the needed information to connect to Zookeeper and use that to establish a connection to HBase.
It also lists the master Pods in `hbase.client.bootstrap.servers`, which HBase 3.x clients use by default to connect (RPC-based connection registry).
These addresses are cluster-internal DNS names, so they only resolve for clients running inside the Kubernetes cluster.

=== Kerberos
In case Kerberos is enabled according to the xref:usage-guide/security.adoc[security documentation], the discovery ConfigMap also includes the information that clients must authenticate themselves using Kerberos.
Expand Down
162 changes: 159 additions & 3 deletions rust/operator-binary/src/controller/build/properties/hbase_site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

use std::collections::BTreeMap;

use stackable_operator::v2::config_overrides::KeyValueConfigOverrides;
use stackable_operator::{
utils::cluster_info::KubernetesClusterInfo, v2::config_overrides::KeyValueConfigOverrides,
};

use crate::{
controller::build::{opa::HbaseOpaConfig, properties::build_xml_config},
controller::{
ValidatedCluster,
build::{opa::HbaseOpaConfig, properties::build_xml_config},
},
crd::{
AnyServiceConfig, HBASE_CLUSTER_DISTRIBUTED, HBASE_MASTER_PORT, HBASE_MASTER_UI_PORT,
HBASE_REGIONSERVER_PORT, HBASE_REGIONSERVER_UI_PORT, HBASE_ROOTDIR, HbaseRole,
Expand Down Expand Up @@ -38,20 +43,61 @@ const ENV_HBASE_SERVICE_HOST: &str = "${env:HBASE_SERVICE_HOST}";
const ENV_HBASE_SERVICE_PORT: &str = "${env:HBASE_SERVICE_PORT}";
const ENV_HBASE_INFO_PORT: &str = "${env:HBASE_INFO_PORT}";

/// Bootstrap nodes for the RPC-based connection registry, the client default since HBase 3.0.0.
/// HBase 2.x defaults to the ZooKeeper-based registry and does not read this key.
pub const HBASE_CLIENT_BOOTSTRAP_SERVERS: &str = "hbase.client.bootstrap.servers";

/// Lists every master Pod as `<sts>-<n>.<headless>.<ns>.svc.<domain>:<port>`.
///
/// A role group's headless Service name is not sufficient: the client resolves each entry to a
/// single address and only fails over between entries, and the headless Service also publishes
/// not-ready Pods.
pub fn client_bootstrap_servers(
cluster: &ValidatedCluster,
cluster_info: &KubernetesClusterInfo,
) -> String {
let namespace = cluster.namespace.as_ref();
let cluster_domain = &cluster_info.cluster_domain;
cluster
.role_group_configs
.get(&HbaseRole::Master)
.into_iter()
.flatten()
.flat_map(|(role_group_name, role_group)| {
let resource_names =
cluster.role_group_resource_names(&HbaseRole::Master, role_group_name);
let stateful_set = resource_names.stateful_set_name();
let headless_service = resource_names.headless_service_name();
// `None` leaves `replicas` unset on the StatefulSet, which Kubernetes defaults to 1
(0..role_group.replicas.unwrap_or(1)).map(move |ordinal| {
format!(
"{stateful_set}-{ordinal}.{headless_service}.{namespace}.svc.{cluster_domain}:{HBASE_MASTER_PORT}"
)
})
})
.collect::<Vec<_>>()
.join(",")
}

/// Renders `hbase-site.xml`.
pub fn build(
role: &HbaseRole,
merged_config: &AnyServiceConfig,
zookeeper_config: BTreeMap<String, String>,
kerberos_config: BTreeMap<String, String>,
opa_config: Option<&HbaseOpaConfig>,
client_bootstrap_servers: String,
overrides: KeyValueConfigOverrides,
) -> String {
let mut config: BTreeMap<String, String> = BTreeMap::new();

// Defaults
config.insert(HBASE_CLUSTER_DISTRIBUTED.to_string(), "true".to_string());
config.insert(HBASE_ROOTDIR.to_string(), merged_config.hbase_rootdir());
config.insert(
HBASE_CLIENT_BOOTSTRAP_SERVERS.to_string(),
client_bootstrap_servers,
);

config.extend(zookeeper_config);
config.extend(kerberos_config);
Expand Down Expand Up @@ -140,7 +186,71 @@ mod tests {
use indoc::indoc;

use super::*;
use crate::test_utils::{merged_config, validated_cluster};
use crate::test_utils::{
cluster_info, hbase_from_yaml, merged_config, validated_cluster, validated_cluster_from,
};

fn bootstrap_servers_for(masters_yaml: &str) -> String {
let hbase = hbase_from_yaml(&format!(
r#"
---
apiVersion: hbase.stackable.tech/v1alpha1
kind: HbaseCluster
metadata:
name: hbase
namespace: default
uid: c2c8c5c0-0b5a-4b1e-9f3e-1a2b3c4d5e6f
spec:
image:
productVersion: 2.6.3
clusterConfig:
hdfsConfigMapName: simple-hdfs
zookeeperConfigMapName: simple-znode
masters:
roleGroups:
{masters_yaml}
regionServers:
roleGroups:
default:
replicas: 1
restServers:
roleGroups:
default:
replicas: 1
"#
));
client_bootstrap_servers(&validated_cluster_from(&hbase), &cluster_info())
}

// The role-group lines are spliced under `roleGroups:` and must be 6 spaces deep, so they are
// written with explicit indentation (`indoc!` would strip it).
#[test]
fn bootstrap_servers_list_every_master_pod() {
let servers = bootstrap_servers_for(
" default:\n replicas: 2\n other:\n replicas: 1",
);
assert_eq!(
servers,
"hbase-master-default-0.hbase-master-default-headless.default.svc.cluster.local:16000,\
hbase-master-default-1.hbase-master-default-headless.default.svc.cluster.local:16000,\
hbase-master-other-0.hbase-master-other-headless.default.svc.cluster.local:16000"
);
}

#[test]
fn bootstrap_servers_default_to_one_replica() {
let servers = bootstrap_servers_for(" default: {}");
assert_eq!(
servers,
"hbase-master-default-0.hbase-master-default-headless.default.svc.cluster.local:16000"
);
}

#[test]
fn bootstrap_servers_skip_scaled_down_role_groups() {
let servers = bootstrap_servers_for(" default:\n replicas: 0");
assert_eq!(servers, "");
}

#[test]
fn renders_operator_defaults() {
Expand All @@ -152,6 +262,7 @@ mod tests {
BTreeMap::new(),
BTreeMap::new(),
None,
String::new(),
KeyValueConfigOverrides::default(),
);
assert!(
Expand All @@ -178,6 +289,7 @@ mod tests {
BTreeMap::new(),
BTreeMap::new(),
None,
String::new(),
KeyValueConfigOverrides::default(),
);
assert!(
Expand All @@ -204,6 +316,7 @@ mod tests {
BTreeMap::new(),
BTreeMap::new(),
None,
String::new(),
KeyValueConfigOverrides::default(),
);
assert!(
Expand All @@ -214,6 +327,48 @@ mod tests {
);
}

#[test]
fn renders_client_bootstrap_servers() {
let validated_cluster = validated_cluster();
let merged = merged_config(&validated_cluster, &HbaseRole::RestServer);
let xml = build(
&HbaseRole::RestServer,
merged,
BTreeMap::new(),
BTreeMap::new(),
None,
"m-0.m-headless.ns.svc.cluster.local:16000".to_string(),
KeyValueConfigOverrides::default(),
);
assert!(
xml.contains(indoc! {"
<name>hbase.client.bootstrap.servers</name>
<value>m-0.m-headless.ns.svc.cluster.local:16000</value>"}),
"{xml}"
);
}

#[test]
fn user_override_wins_for_client_bootstrap_servers() {
let validated_cluster = validated_cluster();
let merged = merged_config(&validated_cluster, &HbaseRole::RestServer);
let xml = build(
&HbaseRole::RestServer,
merged,
BTreeMap::new(),
BTreeMap::new(),
None,
"m-0.m-headless.ns.svc.cluster.local:16000".to_string(),
[("hbase.client.bootstrap.servers", "custom:16000")].into(),
);
assert!(
xml.contains(indoc! {"
<name>hbase.client.bootstrap.servers</name>
<value>custom:16000</value>"}),
"{xml}"
);
}

#[test]
fn user_override_wins() {
let validated_cluster = validated_cluster();
Expand All @@ -224,6 +379,7 @@ mod tests {
BTreeMap::new(),
BTreeMap::new(),
None,
String::new(),
[("hbase.cluster.distributed", "false")].into(),
);
assert!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub fn build_rolegroup_config_map(
.as_hbase_settings(),
kerberos::hbase_site_kerberos_config(cluster, cluster_info),
cluster_config.hbase_opa_config.as_ref(),
hbase_site::client_bootstrap_servers(cluster, cluster_info),
overrides.hbase_site_xml.clone(),
);

Expand Down
91 changes: 87 additions & 4 deletions rust/operator-binary/src/controller/build/resource/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use crate::{
controller::{
ValidatedCluster,
build::{
kerberos, object_meta, properties::ConfigFileName,
kerberos, object_meta,
properties::{ConfigFileName, hbase_site},
recommended_labels_for_role_resources,
},
},
Expand All @@ -34,10 +35,14 @@ pub fn build_discovery_config_map(
) -> Result<ConfigMap> {
let cluster_config = &cluster.cluster_config;

let mut hbase_site = cluster_config
let mut hbase_site_config = cluster_config
.zookeeper_connection_information
.as_hbase_settings();
hbase_site.extend(kerberos::discovery_kerberos_config(cluster, cluster_info));
hbase_site_config.extend(kerberos::discovery_kerberos_config(cluster, cluster_info));
hbase_site_config.insert(
hbase_site::HBASE_CLIENT_BOOTSTRAP_SERVERS.to_string(),
hbase_site::client_bootstrap_servers(cluster, cluster_info),
);

ConfigMapBuilder::new()
.metadata(
Expand All @@ -53,8 +58,86 @@ pub fn build_discovery_config_map(
)
.add_data(
ConfigFileName::HbaseSite.to_string(),
to_hadoop_xml(hbase_site.iter()),
to_hadoop_xml(hbase_site_config.iter()),
)
.build()
.context(BuildConfigMapSnafu)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{
cluster_info, hbase_from_yaml, validated_cluster, validated_cluster_from,
};

#[test]
fn discovery_config_map_keeps_kerberos_settings_next_to_client_bootstrap_servers() {
let hbase = hbase_from_yaml(
r#"
apiVersion: hbase.stackable.tech/v1alpha1
kind: HbaseCluster
metadata:
name: hbase
namespace: default
uid: c2c8c5c0-0b5a-4b1e-9f3e-1a2b3c4d5e6f
spec:
image:
productVersion: 2.6.3
clusterConfig:
hdfsConfigMapName: simple-hdfs
zookeeperConfigMapName: simple-znode
authentication:
tlsSecretClass: tls
kerberos:
secretClass: kerberos-simple
masters:
roleGroups:
default:
replicas: 1
regionServers:
roleGroups:
default:
replicas: 1
restServers:
roleGroups:
default:
replicas: 1
"#,
);
let config_map =
build_discovery_config_map(&validated_cluster_from(&hbase), &cluster_info())
.expect("discovery ConfigMap builds");
let hbase_site = &config_map.data.expect("data is set")["hbase-site.xml"];
assert!(
hbase_site.contains("<name>hbase.security.authentication</name>"),
"{hbase_site}"
);
assert!(
hbase_site.contains("<name>hbase.client.bootstrap.servers</name>"),
"{hbase_site}"
);
}

#[test]
fn discovery_config_map_contains_client_bootstrap_servers() {
let config_map = build_discovery_config_map(&validated_cluster(), &cluster_info())
.expect("discovery ConfigMap builds");
let hbase_site = &config_map.data.expect("data is set")["hbase-site.xml"];
assert!(
hbase_site.contains("<name>hbase.client.bootstrap.servers</name>"),
"{hbase_site}"
);
assert!(
hbase_site.contains(
"<value>hbase-master-default-0.hbase-master-default-headless.default.svc.cluster.local:16000</value>"
),
"{hbase_site}"
);
// the existing ZooKeeper settings are still there
assert!(
hbase_site.contains("<name>hbase.zookeeper.quorum</name>"),
"{hbase_site}"
);
}
}
4 changes: 2 additions & 2 deletions tests/templates/kuttl/kerberos/42-test-rest-server.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ commands:
- /bin/bash
- -c
- |
status_code=\$(curl --write-out '%{http_code}' --silent --insecure --output /dev/null "https://hbase-restserver-default-headless:8080")
status_code=\$(curl --write-out '%{http_code}' --silent --insecure --output /dev/null "https://hbase-restserver-default-headless.$NAMESPACE.svc.cluster.local:8080")

if [[ "\$status_code" -eq 401 ]] ; then
echo "[PASS] Successfully got 401 as we did not authenticate"
Expand All @@ -31,7 +31,7 @@ commands:

kinit -kt /stackable/kerberos/keytab testuser/test-rest-server.$NAMESPACE.svc.cluster.local@{{ test_scenario['values']['kerberos-realm'] }}

status_code=\$(curl --write-out '%{http_code}' --silent --insecure --output /dev/null --negotiate --user : "https://hbase-restserver-default-headless:8080")
status_code=\$(curl --write-out '%{http_code}' --silent --insecure --output /dev/null --negotiate --user : "https://hbase-restserver-default-headless.$NAMESPACE.svc.cluster.local:8080")

if [[ "\$status_code" -eq 200 ]] ; then
echo "[PASS] Successfully got 200 as we did authenticate"
Expand Down
Loading
Loading