Skip to content

Commit d3587d1

Browse files
committed
refactor: split opa/s3 into fetch & validate
1 parent 3945cd3 commit d3587d1

3 files changed

Lines changed: 87 additions & 122 deletions

File tree

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

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
11
//! The dereference step in the DruidCluster controller
2-
//!
3-
//! Fetches all Kubernetes objects referenced by the DruidCluster spec and returns them in
4-
//! [`DereferencedObjects`]. AuthenticationClasses are fetched raw here
5-
//! ([`fetch_authentication_classes`]) and validated later in the validate step. The remaining
6-
//! helpers (`DruidCluster::get_s3_connection`, `S3Bucket::resolve`,
7-
//! `OpaConfig::full_document_url_from_config_map`) still mix fetching and validation; their
8-
//! outputs are treated as "dereferenced" for now. Splitting those is a follow-up.
92
103
use snafu::{OptionExt, ResultExt, Snafu};
114
use stackable_operator::{
@@ -50,11 +43,13 @@ pub enum Error {
5043
cm_name: String,
5144
},
5245

53-
#[snafu(display("failed to get valid S3 connection"))]
54-
GetS3Connection { source: crate::crd::Error },
46+
#[snafu(display("failed to resolve the ingestion S3 connection"))]
47+
ResolveS3Connection {
48+
source: stackable_operator::crd::s3::v1alpha1::ConnectionError,
49+
},
5550

56-
#[snafu(display("failed to get deep storage bucket"))]
57-
GetDeepStorageBucket {
51+
#[snafu(display("failed to resolve the deep storage S3 bucket"))]
52+
ResolveS3Bucket {
5853
source: stackable_operator::crd::s3::v1alpha1::BucketError,
5954
},
6055

@@ -66,13 +61,19 @@ pub enum Error {
6661

6762
type Result<T, E = Error> = std::result::Result<T, E>;
6863

69-
/// Kubernetes objects referenced from the DruidCluster spec, already fetched (and, for now,
70-
/// partly validated by the existing helper functions).
64+
/// Kubernetes objects referenced from the DruidCluster spec, already fetched. Validation and
65+
/// derivation of final values happens in the validate step.
7166
pub struct DereferencedObjects {
7267
pub zookeeper_connection_string: String,
73-
pub opa_connection_string: Option<String>,
74-
pub s3_connection: Option<s3::v1alpha1::ConnectionSpec>,
75-
pub deep_storage_bucket_name: Option<String>,
68+
/// The rule-agnostic OPA document URL (package level, no rule). The validate step appends the
69+
/// concrete authorization rule to produce the connection string.
70+
pub opa_base_document_url: Option<String>,
71+
/// The resolved ingestion S3 connection (if any). The validate step checks it against the deep
72+
/// storage connection.
73+
pub s3_ingestion_connection: Option<s3::v1alpha1::ConnectionSpec>,
74+
/// The resolved deep storage S3 bucket (if deep storage uses S3). Carries both the bucket name
75+
/// and its connection.
76+
pub s3_deep_storage_bucket: Option<s3::v1alpha1::ResolvedBucket>,
7677
/// The raw, fetched `AuthenticationClass` objects (in spec order). Validation of these happens
7778
/// in the validate step via [`AuthenticationClassesResolved::from_fetched`].
7879
pub authentication_classes: Vec<core::v1alpha1::AuthenticationClass>,
@@ -106,12 +107,14 @@ pub async fn dereference(
106107
cm_name: zk_confmap.clone(),
107108
})?;
108109

109-
let opa_connection_string = if let Some(DruidAuthorization { opa: opa_config }) =
110+
// Fetch the rule-agnostic OPA document URL (package level, no rule). The validate step appends
111+
// the concrete authorization rule.
112+
let opa_base_document_url = if let Some(DruidAuthorization { opa: opa_config }) =
110113
&druid.spec.cluster_config.authorization
111114
{
112115
Some(
113116
opa_config
114-
.full_document_url_from_config_map(client, druid, Some("allow"), &OpaApiVersion::V1)
117+
.full_document_url_from_config_map(client, druid, None, &OpaApiVersion::V1)
115118
.await
116119
.context(GetOpaConnStringSnafu {
117120
cm_name: opa_config.config_map_name.clone(),
@@ -121,20 +124,34 @@ pub async fn dereference(
121124
None
122125
};
123126

124-
let s3_connection = druid
125-
.get_s3_connection(client)
126-
.await
127-
.context(GetS3ConnectionSnafu)?;
127+
// Resolve the ingestion and deep storage S3 references separately. Checking that they are
128+
// compatible (and extracting the bucket name) is done in the validate step.
129+
let s3_ingestion_connection = if let Some(ingestion_connection) = druid
130+
.spec
131+
.cluster_config
132+
.ingestion
133+
.as_ref()
134+
.and_then(|ingestion| ingestion.s3connection.as_ref())
135+
{
136+
Some(
137+
ingestion_connection
138+
.clone()
139+
.resolve(client, namespace)
140+
.await
141+
.context(ResolveS3ConnectionSnafu)?,
142+
)
143+
} else {
144+
None
145+
};
128146

129-
let deep_storage_bucket_name = match &druid.spec.cluster_config.deep_storage {
147+
let s3_deep_storage_bucket = match &druid.spec.cluster_config.deep_storage {
130148
DeepStorageSpec::S3(s3_spec) => Some(
131149
s3_spec
132150
.bucket
133151
.clone()
134152
.resolve(client, namespace)
135153
.await
136-
.context(GetDeepStorageBucketSnafu)?
137-
.bucket_name,
154+
.context(ResolveS3BucketSnafu)?,
138155
),
139156
_ => None,
140157
};
@@ -145,9 +162,9 @@ pub async fn dereference(
145162

146163
Ok(DereferencedObjects {
147164
zookeeper_connection_string,
148-
opa_connection_string,
149-
s3_connection,
150-
deep_storage_bucket_name,
165+
opa_base_document_url,
166+
s3_ingestion_connection,
167+
s3_deep_storage_bucket,
151168
authentication_classes,
152169
})
153170
}

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

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
str::FromStr,
1010
};
1111

12-
use snafu::{ResultExt, Snafu};
12+
use snafu::{ResultExt, Snafu, ensure};
1313
use stackable_operator::{
1414
builder::meta::ObjectMetaBuilder,
1515
cli::OperatorEnvironmentOptions,
@@ -70,6 +70,11 @@ pub enum Error {
7070
ResolveAuthenticationClasses {
7171
source: crate::crd::authentication::Error,
7272
},
73+
74+
#[snafu(display(
75+
"two differing S3 connections were given (ingestion and deep storage), this is unsupported by Druid"
76+
))]
77+
IncompatibleS3Connections,
7378
}
7479

7580
type Result<T, E = Error> = std::result::Result<T, E>;
@@ -373,16 +378,46 @@ pub fn validate(
373378
.jdbc_connection_details("metadata")
374379
.context(InvalidMetadataDatabaseConnectionSnafu)?;
375380

381+
// The deep storage bucket carries its own S3 connection; if ingestion also configures one, the
382+
// two must be identical, as Druid only supports a single S3 connection.
383+
let s3_deep_storage_connection = dereferenced_objects
384+
.s3_deep_storage_bucket
385+
.as_ref()
386+
.map(|bucket| &bucket.connection);
387+
let s3_connection = match (
388+
dereferenced_objects.s3_ingestion_connection.as_ref(),
389+
s3_deep_storage_connection,
390+
) {
391+
(Some(ingestion), Some(deep_storage)) => {
392+
ensure!(ingestion == deep_storage, IncompatibleS3ConnectionsSnafu);
393+
Some(ingestion.clone())
394+
}
395+
(Some(connection), None) | (None, Some(connection)) => Some(connection.clone()),
396+
(None, None) => None,
397+
};
398+
let deep_storage_bucket_name = dereferenced_objects
399+
.s3_deep_storage_bucket
400+
.as_ref()
401+
.map(|bucket| bucket.bucket_name.clone());
402+
403+
// The dereference step fetched the rule-agnostic OPA document URL; append the authorization
404+
// rule Druid queries.
405+
// TODO(@maltesander): This would rather be a preprocess step?
406+
let opa_connection_string = dereferenced_objects
407+
.opa_base_document_url
408+
.as_ref()
409+
.map(|base_url| format!("{base_url}/allow"));
410+
376411
Ok(ValidatedCluster::new(
377412
name,
378413
namespace,
379414
uid,
380415
image,
381416
ValidatedClusterConfig {
382417
zookeeper_connection_string: dereferenced_objects.zookeeper_connection_string.clone(),
383-
opa_connection_string: dereferenced_objects.opa_connection_string.clone(),
384-
s3_connection: dereferenced_objects.s3_connection.clone(),
385-
deep_storage_bucket_name: dereferenced_objects.deep_storage_bucket_name.clone(),
418+
opa_connection_string,
419+
s3_connection,
420+
deep_storage_bucket_name,
386421
druid_tls_security,
387422
druid_auth_config,
388423
metadata_database: druid.spec.cluster_config.metadata_database.clone(),
@@ -553,9 +588,9 @@ mod tests {
553588
fn dereferenced_objects() -> DereferencedObjects {
554589
DereferencedObjects {
555590
zookeeper_connection_string: "zookeeper-connection-string".to_string(),
556-
opa_connection_string: None,
557-
s3_connection: None,
558-
deep_storage_bucket_name: None,
591+
opa_base_document_url: None,
592+
s3_ingestion_connection: None,
593+
s3_deep_storage_bucket: None,
559594
authentication_classes: Vec::new(),
560595
}
561596
}

rust/operator-binary/src/crd/mod.rs

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use security::add_cert_to_jvm_trust_store_cmd;
88
use serde::{Deserialize, Serialize};
99
use snafu::{OptionExt, ResultExt, Snafu};
1010
use stackable_operator::{
11-
client::Client,
1211
commons::{
1312
affinity::StackableAffinity,
1413
cluster_operation::ClusterOperation,
@@ -152,25 +151,9 @@ pub enum Error {
152151
#[snafu(display("missing secret lifetime"))]
153152
MissingSecretLifetime,
154153

155-
#[snafu(display("failed to resolve S3 connection"))]
156-
ResolveS3Connection {
157-
source: stackable_operator::crd::s3::v1alpha1::ConnectionError,
158-
},
159-
160-
#[snafu(display("failed to resolve S3 bucket"))]
161-
ResolveS3Bucket {
162-
source: stackable_operator::crd::s3::v1alpha1::BucketError,
163-
},
164-
165-
#[snafu(display("2 differing s3 connections were given, this is unsupported by Druid"))]
166-
IncompatibleS3Connections,
167-
168154
#[snafu(display("the role group {rolegroup_name} is not defined"))]
169155
CannotRetrieveRoleGroup { rolegroup_name: String },
170156

171-
#[snafu(display("missing namespace for resource {name}"))]
172-
MissingNamespace { name: String },
173-
174157
#[snafu(display("fragment validation failure"))]
175158
FragmentValidationFailure { source: ValidationError },
176159

@@ -363,76 +346,6 @@ impl HasStatusCondition for v1alpha1::DruidCluster {
363346
}
364347

365348
impl v1alpha1::DruidCluster {
366-
/// If an s3 connection for ingestion is given, as well as an s3 connection for deep storage, they need to be the same.
367-
/// This function returns the resolved connection, or raises an Error if the connections are not identical.
368-
pub async fn get_s3_connection(
369-
&self,
370-
client: &Client,
371-
) -> Result<Option<s3::v1alpha1::ConnectionSpec>, Error> {
372-
// retrieve connection for ingestion (can be None)
373-
let ingestion_conn = if let Some(ic) = self
374-
.spec
375-
.cluster_config
376-
.ingestion
377-
.as_ref()
378-
.and_then(|is| is.s3connection.as_ref())
379-
{
380-
Some(
381-
ic.clone()
382-
.resolve(
383-
client,
384-
self.namespace()
385-
.context(MissingNamespaceSnafu {
386-
name: &self.name_unchecked(),
387-
})?
388-
.as_ref(),
389-
)
390-
.await
391-
.context(ResolveS3ConnectionSnafu)?,
392-
)
393-
} else {
394-
None
395-
};
396-
397-
// retrieve connection for deep storage (can be None)
398-
let storage_conn = match &self.spec.cluster_config.deep_storage {
399-
DeepStorageSpec::S3(s3_spec) => {
400-
let inlined_bucket = s3_spec
401-
.bucket
402-
.clone()
403-
.resolve(
404-
client,
405-
self.namespace()
406-
.context(MissingNamespaceSnafu {
407-
name: &self.name_unchecked(),
408-
})?
409-
.as_ref(),
410-
)
411-
.await
412-
.context(ResolveS3BucketSnafu)?;
413-
Some(inlined_bucket.connection)
414-
}
415-
_ => None,
416-
};
417-
418-
// if both connections are specified and are identical, return it
419-
// if they differ, raise an error
420-
// if only one connection is specified, return it
421-
if ingestion_conn.is_some() && storage_conn.is_some() {
422-
if ingestion_conn == storage_conn {
423-
Ok(ingestion_conn)
424-
} else {
425-
Err(Error::IncompatibleS3Connections)
426-
}
427-
} else if ingestion_conn.is_some() {
428-
Ok(ingestion_conn)
429-
} else if storage_conn.is_some() {
430-
Ok(storage_conn)
431-
} else {
432-
Ok(None)
433-
}
434-
}
435-
436349
/// Merges and validates all role groups of the given role. Invoked from the validate step
437350
/// (`controller::validate`).
438351
///

0 commit comments

Comments
 (0)