Skip to content

Commit 7c07062

Browse files
maltesanderclaude
andcommitted
feat: Add hive-site.xml builder preserving product-config output
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2a3f6f1 commit 7c07062

1 file changed

Lines changed: 292 additions & 1 deletion

File tree

  • rust/operator-binary/src/controller/build/properties
Lines changed: 292 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,292 @@
1-
//! placeholder — implemented in a later task
1+
//! Builder for `hive-site.xml`.
2+
//!
3+
//! Precedence (matches the pre-product-config-removal behaviour):
4+
//! 1. Defaults: `hive.metastore.port=9083` (required product-config property, default value).
5+
//! 2. Automatic / operator-injected: warehouse dir (hardcoded `/stackable/warehouse`),
6+
//! metrics enabled (`true`), JDBC driver/url/credentials, S3, Kerberos, OPA.
7+
//! 3. Spec: `warehouseDir` from the merged config overrides the hardcoded warehouse dir.
8+
//! 4. User `hive-site.xml` overrides (highest precedence).
9+
10+
use std::collections::BTreeMap;
11+
12+
use snafu::{ResultExt, Snafu};
13+
use stackable_operator::{
14+
crd::s3,
15+
database_connections::drivers::jdbc::JdbcDatabaseConnectionDetails,
16+
k8s_openapi::api::core::v1::EnvVar,
17+
utils::cluster_info::KubernetesClusterInfo,
18+
};
19+
20+
use crate::{
21+
config::opa::HiveOpaConfig,
22+
crd::{
23+
MetaStoreConfig,
24+
databases::{MetadataDatabaseConnection, derby_driver_class},
25+
v1alpha1,
26+
},
27+
kerberos::kerberos_config_properties,
28+
};
29+
30+
const DEFAULT_WAREHOUSE_DIR: &str = "/stackable/warehouse";
31+
const HIVE_METASTORE_PORT: &str = "hive.metastore.port";
32+
const DEFAULT_HIVE_METASTORE_PORT: &str = "9083";
33+
34+
#[derive(Debug, Snafu)]
35+
pub enum Error {
36+
#[snafu(display("failed to configure S3 connection"))]
37+
ConfigureS3Connection {
38+
source: stackable_operator::crd::s3::v1alpha1::ConnectionError,
39+
},
40+
}
41+
42+
type Result<T, E = Error> = std::result::Result<T, E>;
43+
44+
/// Build the `hive-site.xml` key/value pairs. Values are `Option` so the writer
45+
/// can skip unset entries (no `None` is produced here, but the type matches the
46+
/// writer's iterator interface).
47+
#[allow(clippy::too_many_arguments)]
48+
pub fn build(
49+
hive: &v1alpha1::HiveCluster,
50+
hive_namespace: &str,
51+
product_version: &str,
52+
merged_config: &MetaStoreConfig,
53+
database_connection_details: &JdbcDatabaseConnectionDetails,
54+
s3_connection_spec: Option<&s3::v1alpha1::ConnectionSpec>,
55+
hive_opa_config: Option<&HiveOpaConfig>,
56+
cluster_info: &KubernetesClusterInfo,
57+
overrides: BTreeMap<String, String>,
58+
) -> Result<BTreeMap<String, Option<String>>> {
59+
let mut data: BTreeMap<String, Option<String>> = BTreeMap::new();
60+
61+
// 1. Defaults (required product-config property `hive.metastore.port`).
62+
data.insert(
63+
HIVE_METASTORE_PORT.to_string(),
64+
Some(DEFAULT_HIVE_METASTORE_PORT.to_string()),
65+
);
66+
67+
// 2. Automatic / operator-injected.
68+
data.insert(
69+
MetaStoreConfig::METASTORE_WAREHOUSE_DIR.to_string(),
70+
Some(DEFAULT_WAREHOUSE_DIR.to_string()),
71+
);
72+
data.insert(
73+
MetaStoreConfig::METASTORE_METRICS_ENABLED.to_string(),
74+
Some("true".to_string()),
75+
);
76+
77+
// The Derby driver class needs special handling.
78+
let driver = match &hive.spec.cluster_config.metadata_database {
79+
MetadataDatabaseConnection::Derby(_) => derby_driver_class(product_version),
80+
_ => database_connection_details.driver.as_str(),
81+
};
82+
data.insert(
83+
MetaStoreConfig::CONNECTION_DRIVER_NAME.to_string(),
84+
Some(driver.to_owned()),
85+
);
86+
data.insert(
87+
MetaStoreConfig::CONNECTION_URL.to_string(),
88+
Some(database_connection_details.connection_url.to_string()),
89+
);
90+
if let Some(EnvVar { name, .. }) = &database_connection_details.username_env {
91+
data.insert(
92+
MetaStoreConfig::CONNECTION_USER_NAME.to_string(),
93+
Some(format!("${{env:{name}}}")),
94+
);
95+
}
96+
if let Some(EnvVar { name, .. }) = &database_connection_details.password_env {
97+
data.insert(
98+
MetaStoreConfig::CONNECTION_PASSWORD.to_string(),
99+
Some(format!("${{env:{name}}}")),
100+
);
101+
}
102+
103+
if let Some(s3) = s3_connection_spec {
104+
data.insert(
105+
MetaStoreConfig::S3_ENDPOINT.to_string(),
106+
Some(s3.endpoint().context(ConfigureS3ConnectionSnafu)?.to_string()),
107+
);
108+
data.insert(
109+
MetaStoreConfig::S3_REGION_NAME.to_string(),
110+
Some(s3.region.name.clone()),
111+
);
112+
if let Some((access_key_file, secret_key_file)) = s3.credentials_mount_paths() {
113+
data.insert(
114+
MetaStoreConfig::S3_ACCESS_KEY.to_string(),
115+
Some(format!("${{file:UTF-8:{access_key_file}}}")),
116+
);
117+
data.insert(
118+
MetaStoreConfig::S3_SECRET_KEY.to_string(),
119+
Some(format!("${{file:UTF-8:{secret_key_file}}}")),
120+
);
121+
}
122+
data.insert(
123+
MetaStoreConfig::S3_SSL_ENABLED.to_string(),
124+
Some(s3.tls.uses_tls().to_string()),
125+
);
126+
data.insert(
127+
MetaStoreConfig::S3_PATH_STYLE_ACCESS.to_string(),
128+
Some((s3.access_style == s3::v1alpha1::S3AccessStyle::Path).to_string()),
129+
);
130+
}
131+
132+
for (name, value) in kerberos_config_properties(hive, hive_namespace, cluster_info) {
133+
data.insert(name.to_string(), Some(value.to_string()));
134+
}
135+
136+
if let Some(opa_config) = hive_opa_config {
137+
data.extend(
138+
opa_config
139+
.as_config(product_version)
140+
.into_iter()
141+
.map(|(k, v)| (k, Some(v))),
142+
);
143+
}
144+
145+
// 3. Spec: warehouse dir from the merged CRD config (overrides the default).
146+
if let Some(warehouse_dir) = &merged_config.warehouse_dir {
147+
data.insert(
148+
MetaStoreConfig::METASTORE_WAREHOUSE_DIR.to_string(),
149+
Some(warehouse_dir.clone()),
150+
);
151+
}
152+
153+
// 4. User overrides (highest precedence).
154+
for (k, v) in overrides {
155+
data.insert(k, Some(v));
156+
}
157+
158+
Ok(data)
159+
}
160+
161+
#[cfg(test)]
162+
mod tests {
163+
use super::*;
164+
165+
fn hive_cluster(yaml: &str) -> v1alpha1::HiveCluster {
166+
stackable_operator::utils::yaml_from_str_singleton_map(yaml).expect("valid HiveCluster YAML")
167+
}
168+
169+
const DERBY_YAML: &str = r#"
170+
apiVersion: hive.stackable.tech/v1alpha1
171+
kind: HiveCluster
172+
metadata:
173+
name: simple-hive
174+
namespace: default
175+
spec:
176+
image:
177+
productVersion: "4.0.0"
178+
clusterConfig:
179+
metadataDatabase:
180+
derby: {}
181+
metastore:
182+
roleGroups:
183+
default:
184+
replicas: 1
185+
"#;
186+
187+
fn cluster_info() -> KubernetesClusterInfo {
188+
KubernetesClusterInfo {
189+
cluster_domain: "cluster.local".parse().expect("valid domain"),
190+
}
191+
}
192+
193+
fn db_details(hive: &v1alpha1::HiveCluster) -> JdbcDatabaseConnectionDetails {
194+
hive.spec
195+
.cluster_config
196+
.metadata_database
197+
.jdbc_connection_details("METADATA")
198+
.expect("derby connection details")
199+
}
200+
201+
#[test]
202+
fn defaults_present_for_minimal_derby_cluster() {
203+
let hive = hive_cluster(DERBY_YAML);
204+
let merged = MetaStoreConfig::default();
205+
let db = db_details(&hive);
206+
207+
let data = build(
208+
&hive,
209+
"default",
210+
"4.0.0",
211+
&merged,
212+
&db,
213+
None,
214+
None,
215+
&cluster_info(),
216+
BTreeMap::new(),
217+
)
218+
.expect("build hive-site");
219+
220+
assert_eq!(
221+
data.get("hive.metastore.port"),
222+
Some(&Some("9083".to_string()))
223+
);
224+
assert_eq!(
225+
data.get("hive.metastore.metrics.enabled"),
226+
Some(&Some("true".to_string()))
227+
);
228+
assert_eq!(
229+
data.get("hive.metastore.warehouse.dir"),
230+
Some(&Some("/stackable/warehouse".to_string()))
231+
);
232+
assert!(data.contains_key("javax.jdo.option.ConnectionDriverName"));
233+
// No env credentials for an embedded Derby database.
234+
assert!(!data.contains_key("javax.jdo.option.ConnectionUserName"));
235+
}
236+
237+
#[test]
238+
fn warehouse_dir_spec_overrides_default() {
239+
let hive = hive_cluster(DERBY_YAML);
240+
let merged = MetaStoreConfig {
241+
warehouse_dir: Some("/custom/warehouse".to_string()),
242+
..MetaStoreConfig::default()
243+
};
244+
let db = db_details(&hive);
245+
246+
let data = build(
247+
&hive,
248+
"default",
249+
"4.0.0",
250+
&merged,
251+
&db,
252+
None,
253+
None,
254+
&cluster_info(),
255+
BTreeMap::new(),
256+
)
257+
.expect("build hive-site");
258+
259+
assert_eq!(
260+
data.get("hive.metastore.warehouse.dir"),
261+
Some(&Some("/custom/warehouse".to_string()))
262+
);
263+
}
264+
265+
#[test]
266+
fn user_override_wins_over_everything() {
267+
let hive = hive_cluster(DERBY_YAML);
268+
let merged = MetaStoreConfig::default();
269+
let db = db_details(&hive);
270+
let overrides = [("hive.metastore.port".to_string(), "1234".to_string())]
271+
.into_iter()
272+
.collect();
273+
274+
let data = build(
275+
&hive,
276+
"default",
277+
"4.0.0",
278+
&merged,
279+
&db,
280+
None,
281+
None,
282+
&cluster_info(),
283+
overrides,
284+
)
285+
.expect("build hive-site");
286+
287+
assert_eq!(
288+
data.get("hive.metastore.port"),
289+
Some(&Some("1234".to_string()))
290+
);
291+
}
292+
}

0 commit comments

Comments
 (0)