Skip to content

Commit 7854f9a

Browse files
committed
add new client protocol module
1 parent ef53a73 commit 7854f9a

3 files changed

Lines changed: 328 additions & 8 deletions

File tree

rust/operator-binary/src/controller.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ use crate::{
8484
DISCOVERY_URI, ENV_INTERNAL_SECRET, EXCHANGE_MANAGER_PROPERTIES, HTTP_PORT, HTTP_PORT_NAME,
8585
HTTPS_PORT, HTTPS_PORT_NAME, JVM_CONFIG, JVM_SECURITY_PROPERTIES, LOG_PROPERTIES,
8686
MAX_TRINO_LOG_FILES_SIZE, METRICS_PORT, METRICS_PORT_NAME, NODE_PROPERTIES,
87-
RW_CONFIG_DIR_NAME, STACKABLE_CLIENT_TLS_DIR, STACKABLE_INTERNAL_TLS_DIR,
88-
STACKABLE_MOUNT_INTERNAL_TLS_DIR, STACKABLE_MOUNT_SERVER_TLS_DIR, STACKABLE_SERVER_TLS_DIR,
89-
TrinoRole,
87+
RW_CONFIG_DIR_NAME, SPOOLING_MANAGER_PROPERTIES, STACKABLE_CLIENT_TLS_DIR,
88+
STACKABLE_INTERNAL_TLS_DIR, STACKABLE_MOUNT_INTERNAL_TLS_DIR,
89+
STACKABLE_MOUNT_SERVER_TLS_DIR, STACKABLE_SERVER_TLS_DIR, TrinoRole,
9090
authentication::resolve_authentication_classes,
91-
catalog,
91+
catalog, client_protocol,
9292
discovery::{TrinoDiscovery, TrinoDiscoveryProtocol, TrinoPodRef},
9393
fault_tolerant_execution::ResolvedFaultTolerantExecutionConfig,
9494
rolegroup_headless_service_name, v1alpha1,
@@ -133,6 +133,12 @@ pub enum Error {
133133
#[snafu(display("object defines no namespace"))]
134134
ObjectHasNoNamespace,
135135

136+
#[snafu(display("Trino cluster {} has no namespace", name))]
137+
MissingTrinoNamespace {
138+
source: crate::crd::Error,
139+
name: String,
140+
},
141+
136142
#[snafu(display("object defines no {role:?} role"))]
137143
MissingTrinoRole {
138144
source: crate::crd::Error,
@@ -374,6 +380,9 @@ pub enum Error {
374380
ResolveProductImage {
375381
source: product_image_selection::Error,
376382
},
383+
384+
#[snafu(display("failed to resolve client protocol configuration"))]
385+
ClientProtocolConfiguration { source: client_protocol::Error },
377386
}
378387

379388
type Result<T, E = Error> = std::result::Result<T, E>;
@@ -397,6 +406,10 @@ pub async fn reconcile_trino(
397406
.context(InvalidTrinoClusterSnafu)?;
398407
let client = &ctx.client;
399408

409+
let namespace = trino.namespace_r().context(MissingTrinoNamespaceSnafu {
410+
name: trino.name_any(),
411+
})?;
412+
400413
let resolved_product_image = trino
401414
.spec
402415
.image
@@ -443,13 +456,23 @@ pub async fn reconcile_trino(
443456
// Resolve fault tolerant execution configuration with S3 connections if needed
444457
let resolved_fte_config = match trino.spec.cluster_config.fault_tolerant_execution.as_ref() {
445458
Some(fte_config) => Some(
446-
ResolvedFaultTolerantExecutionConfig::from_config(
447-
fte_config,
459+
ResolvedFaultTolerantExecutionConfig::from_config(fte_config, Some(client), &namespace)
460+
.await
461+
.context(FaultTolerantExecutionSnafu)?,
462+
),
463+
None => None,
464+
};
465+
466+
// Resolve client spooling protocol configuration with S3 connections if needed
467+
let resolved_spooling_config = match trino.spec.cluster_config.client_protocol.as_ref() {
468+
Some(client_protocol_config) => Some(
469+
client_protocol::ResolvedSpoolingProtocolConfig::from_config(
470+
&client_protocol_config.spooling,
448471
Some(client),
449-
&trino.namespace_r().context(ReadRoleSnafu)?,
472+
&namespace,
450473
)
451474
.await
452-
.context(FaultTolerantExecutionSnafu)?,
475+
.context(ClientProtocolConfigurationSnafu)?,
453476
),
454477
None => None,
455478
};
@@ -557,6 +580,7 @@ pub async fn reconcile_trino(
557580
&trino_opa_config,
558581
&client.kubernetes_cluster_info,
559582
&resolved_fte_config,
583+
&resolved_spooling_config,
560584
)?;
561585
let rg_catalog_configmap = build_rolegroup_catalog_config_map(
562586
trino,
@@ -684,6 +708,7 @@ fn build_rolegroup_config_map(
684708
trino_opa_config: &Option<TrinoOpaConfig>,
685709
cluster_info: &KubernetesClusterInfo,
686710
resolved_fte_config: &Option<ResolvedFaultTolerantExecutionConfig>,
711+
resolved_spooling_protocol_config: &Option<client_protocol::ResolvedSpoolingProtocolConfig>,
687712
) -> Result<ConfigMap> {
688713
let mut cm_conf_data = BTreeMap::new();
689714

@@ -835,6 +860,23 @@ fn build_rolegroup_config_map(
835860
}
836861
}
837862

863+
// Add client protocol properties (especially spooling properties)
864+
if let Some(resolved_client_protocol) = resolved_spooling_protocol_config {
865+
if resolved_client_protocol.is_enabled() {
866+
let spooling_props_with_options: BTreeMap<String, Option<String>> =
867+
resolved_client_protocol
868+
.spooling_manager_properties
869+
.iter()
870+
.map(|(k, v)| (k.clone(), Some(v.clone())))
871+
.collect();
872+
cm_conf_data.insert(
873+
SPOOLING_MANAGER_PROPERTIES.to_string(),
874+
to_java_properties_string(spooling_props_with_options.iter())
875+
.with_context(|_| FailedToWriteJavaPropertiesSnafu)?,
876+
);
877+
}
878+
}
879+
838880
let jvm_sec_props: BTreeMap<String, Option<String>> = config
839881
.get(&PropertyNameKind::File(JVM_SECURITY_PROPERTIES.to_string()))
840882
.cloned()
@@ -1864,6 +1906,7 @@ mod tests {
18641906
&trino_opa_config,
18651907
&cluster_info,
18661908
&None,
1909+
&None,
18671910
)
18681911
.unwrap()
18691912
}
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
use std::collections::{BTreeMap, HashMap};
2+
3+
/// This module manages the client protocol properties, especially the for spooling.
4+
/// Trino documentation is available here: https://trino.io/docs/current/client/client-protocol.html
5+
use serde::{Deserialize, Serialize};
6+
use snafu::Snafu;
7+
use stackable_operator::{
8+
client::Client,
9+
commons::tls_verification::{CaCert, TlsServerVerification, TlsVerification},
10+
crd::s3,
11+
k8s_openapi::{
12+
api::core::v1::{Volume, VolumeMount},
13+
apimachinery::pkg::api::resource::Quantity,
14+
},
15+
schemars::{self, JsonSchema},
16+
shared::time::Duration,
17+
};
18+
use strum::Display;
19+
20+
use crate::{command, crd::STACKABLE_CLIENT_TLS_DIR};
21+
22+
const SPOOLING_S3_AWS_ACCESS_KEY: &str = "SPOOLING_S3_AWS_ACCESS_KEY";
23+
const SPOOLING_S3_AWS_SECRET_KEY: &str = "SPOOLING_S3_AWS_SECRET_KEY";
24+
25+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
26+
#[serde(rename_all = "camelCase")]
27+
pub struct ClientProtocolConfig {
28+
#[serde(flatten)]
29+
pub spooling: SpoolingProtocolConfig,
30+
}
31+
32+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
33+
#[serde(rename_all = "camelCase")]
34+
pub struct SpoolingProtocolConfig {
35+
// Spooling protocol properties
36+
/// Enable spooling protocol.
37+
pub enabled: bool,
38+
39+
// Name of the Kubernetes Secret with one entry ("key")
40+
// to use as protocol.spooling.shared-secret-key property
41+
pub shared_secret: String,
42+
43+
// Segment retrieval mode used by clients.
44+
#[serde(skip_serializing_if = "Option::is_none")]
45+
pub retrieval_mode: Option<SpoolingRetrievalMode>,
46+
47+
// Spooled segment size. Is translated to both initial and max segment size.
48+
// Use overrides for set those explicitly to distinct values.
49+
#[serde(skip_serializing_if = "Option::is_none")]
50+
pub segment_size: Option<Quantity>,
51+
52+
// Spooling filesystem properties
53+
54+
// Spool segment location. Each Trino cluster must have its own
55+
// location independent of any other clusters.
56+
pub location: String,
57+
58+
// Spool segment TTL. Is translated to both fs.segment.ttl as well as
59+
// fs.segment.direct.ttl.
60+
// Use overrides for set those explicitly to distinct values.
61+
#[serde(skip_serializing_if = "Option::is_none")]
62+
pub segment_ttl: Option<Duration>,
63+
64+
// Spool segment encryption.
65+
#[serde(skip_serializing_if = "Option::is_none")]
66+
pub segment_encryption: Option<bool>,
67+
68+
// Spooling filesystem properties. Only S3 is supported.
69+
#[serde(flatten)]
70+
pub filesystem: SpoolingFileSystemConfig,
71+
72+
/// The `configOverrides` allow overriding arbitrary client protocol properties.
73+
#[serde(default)]
74+
pub config_overrides: HashMap<String, String>,
75+
}
76+
77+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize, Display)]
78+
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
79+
pub enum SpoolingRetrievalMode {
80+
Storage,
81+
CoordinatorStorageRedirect,
82+
CoordinatorProxy,
83+
WorkerProxy,
84+
}
85+
86+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
87+
pub enum SpoolingFileSystemConfig {
88+
S3(S3SpoolingConfig),
89+
}
90+
// TODO: this is exactly the same as fault_tolerant_execution::S3ExchangeConfig
91+
// but without the base_directory property.
92+
// Consolidate Trino S3 properties in a single reusable struct.
93+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
94+
#[serde(rename_all = "camelCase")]
95+
pub struct S3SpoolingConfig {
96+
/// S3 connection configuration.
97+
/// Learn more about S3 configuration in the [S3 concept docs](DOCS_BASE_URL_PLACEHOLDER/concepts/s3).
98+
pub connection: stackable_operator::crd::s3::v1alpha1::InlineConnectionOrReference,
99+
100+
/// IAM role to assume for S3 access.
101+
#[serde(skip_serializing_if = "Option::is_none")]
102+
pub iam_role: Option<String>,
103+
104+
/// External ID for the IAM role trust policy.
105+
#[serde(skip_serializing_if = "Option::is_none")]
106+
pub external_id: Option<String>,
107+
108+
/// Maximum number of times the S3 client should retry a request.
109+
#[serde(skip_serializing_if = "Option::is_none")]
110+
pub max_error_retries: Option<u32>,
111+
112+
/// Part data size for S3 multi-part upload.
113+
#[serde(skip_serializing_if = "Option::is_none")]
114+
pub upload_part_size: Option<Quantity>,
115+
}
116+
117+
pub struct ResolvedSpoolingProtocolConfig {
118+
/// Enable spooling protocol.
119+
pub enabled: bool,
120+
121+
// Properties for spooling-manager.properties
122+
pub spooling_manager_properties: BTreeMap<String, String>,
123+
124+
/// Volumes required for the configuration (e.g., for S3 credentials)
125+
pub volumes: Vec<Volume>,
126+
127+
/// Volume mounts required for the configuration
128+
pub volume_mounts: Vec<VolumeMount>,
129+
130+
/// Env-Vars that should be exported from files.
131+
/// You can think of it like `export <key>="$(cat <value>)"`
132+
pub load_env_from_files: BTreeMap<String, String>,
133+
134+
/// Additional commands that need to be executed before starting Trino
135+
/// Used to add TLS certificates to the client's trust store.
136+
pub init_container_extra_start_commands: Vec<String>,
137+
}
138+
139+
impl ResolvedSpoolingProtocolConfig {
140+
/// Resolve S3 connection properties from Kubernetes resources
141+
/// and prepare spooling filesystem configuration.
142+
pub async fn from_config(
143+
config: &SpoolingProtocolConfig,
144+
client: Option<&Client>,
145+
namespace: &str,
146+
) -> Result<Self, Error> {
147+
let spooling_manager_properties = BTreeMap::new();
148+
149+
let mut resolved_config = Self {
150+
enabled: config.enabled,
151+
spooling_manager_properties,
152+
volumes: Vec::new(),
153+
volume_mounts: Vec::new(),
154+
load_env_from_files: BTreeMap::new(),
155+
init_container_extra_start_commands: Vec::new(),
156+
};
157+
158+
// Resolve external resources if Kubernetes client is available
159+
// This should always be the case, except for when this function is called during unit tests
160+
if let Some(client) = client {
161+
match &config.filesystem {
162+
SpoolingFileSystemConfig::S3(s3_config) => {
163+
resolved_config
164+
.resolve_s3_backend(s3_config, client, namespace)
165+
.await?;
166+
}
167+
}
168+
}
169+
170+
Ok(resolved_config)
171+
}
172+
173+
async fn resolve_s3_backend(
174+
&mut self,
175+
s3_config: &S3SpoolingConfig,
176+
client: &Client,
177+
namespace: &str,
178+
) -> Result<(), Error> {
179+
use snafu::ResultExt;
180+
181+
let s3_connection = s3_config
182+
.connection
183+
.clone()
184+
.resolve(client, namespace)
185+
.await
186+
.context(S3ConnectionSnafu)?;
187+
188+
let (volumes, mounts) = s3_connection
189+
.volumes_and_mounts()
190+
.context(S3ConnectionSnafu)?;
191+
self.volumes.extend(volumes);
192+
self.volume_mounts.extend(mounts);
193+
194+
self.spooling_manager_properties
195+
.insert("s3.region".to_string(), s3_connection.region.name.clone());
196+
self.spooling_manager_properties.insert(
197+
"s3.endpoint".to_string(),
198+
s3_connection
199+
.endpoint()
200+
.context(S3ConnectionSnafu)?
201+
.to_string(),
202+
);
203+
self.spooling_manager_properties.insert(
204+
"s3.path-style-access".to_string(),
205+
(s3_connection.access_style == s3::v1alpha1::S3AccessStyle::Path).to_string(),
206+
);
207+
208+
if let Some((access_key_path, secret_key_path)) = s3_connection.credentials_mount_paths() {
209+
self.spooling_manager_properties.extend([
210+
(
211+
"s3.aws-access-key".to_string(),
212+
format!("${{ENV:{SPOOLING_S3_AWS_ACCESS_KEY}}}"),
213+
),
214+
(
215+
"s3.aws-secret-key".to_string(),
216+
format!("${{ENV:{SPOOLING_S3_AWS_SECRET_KEY}}}"),
217+
),
218+
]);
219+
220+
self.load_env_from_files.extend([
221+
(String::from(SPOOLING_S3_AWS_ACCESS_KEY), access_key_path),
222+
(String::from(SPOOLING_S3_AWS_SECRET_KEY), secret_key_path),
223+
]);
224+
}
225+
226+
if let Some(tls) = s3_connection.tls.tls.as_ref() {
227+
match &tls.verification {
228+
TlsVerification::None {} => return S3TlsNoVerificationNotSupportedSnafu.fail(),
229+
TlsVerification::Server(TlsServerVerification {
230+
ca_cert: CaCert::WebPki {},
231+
}) => {}
232+
TlsVerification::Server(TlsServerVerification {
233+
ca_cert: CaCert::SecretClass(_),
234+
}) => {
235+
if let Some(ca_cert) = s3_connection.tls.tls_ca_cert_mount_path() {
236+
self.init_container_extra_start_commands.extend(
237+
command::add_cert_to_truststore(
238+
&ca_cert,
239+
STACKABLE_CLIENT_TLS_DIR,
240+
"spooling-s3-ca-cert",
241+
),
242+
);
243+
}
244+
}
245+
}
246+
}
247+
248+
Ok(())
249+
}
250+
251+
pub(crate) fn is_enabled(&self) -> bool {
252+
return self.enabled;
253+
}
254+
}
255+
256+
#[derive(Snafu, Debug)]
257+
pub enum Error {
258+
#[snafu(display("Failed to resolve S3 connection"))]
259+
S3Connection {
260+
source: s3::v1alpha1::ConnectionError,
261+
},
262+
263+
#[snafu(display("trino does not support disabling the TLS verification of S3 servers"))]
264+
S3TlsNoVerificationNotSupported,
265+
266+
#[snafu(display("failed to convert data size for [{field}] to bytes"))]
267+
QuantityConversion {
268+
source: stackable_operator::memory::Error,
269+
field: &'static str,
270+
},
271+
}

0 commit comments

Comments
 (0)