Skip to content

Commit 61ab5af

Browse files
committed
feat: add validate and dereference steps
1 parent 9b8f1ad commit 61ab5af

7 files changed

Lines changed: 412 additions & 321 deletions

File tree

rust/operator-binary/src/controller.rs

Lines changed: 35 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::{
44
borrow::Cow,
5-
collections::{BTreeMap, HashMap, HashSet},
5+
collections::{BTreeMap, HashMap},
66
sync::Arc,
77
};
88

@@ -27,10 +27,7 @@ use stackable_operator::{
2727
cli::OperatorEnvironmentOptions,
2828
client::Client,
2929
cluster_resources::{ClusterResourceApplyStrategy, ClusterResources},
30-
commons::{
31-
product_image_selection::{self, ResolvedProductImage},
32-
rbac::build_rbac_resources,
33-
},
30+
commons::{product_image_selection::ResolvedProductImage, rbac::build_rbac_resources},
3431
constants::RESTART_CONTROLLER_ENABLED_LABEL,
3532
crd::{authentication::oidc::v1alpha1::AuthenticationProvider, git_sync},
3633
k8s_openapi::{
@@ -75,20 +72,21 @@ use stackable_operator::{
7572
use strum::{EnumDiscriminants, IntoStaticStr};
7673
use tracing::Instrument;
7774

75+
mod dereference;
76+
mod validate;
77+
7878
use crate::{
7979
OPERATOR_NAME,
8080
config::{
8181
self, JVM_SECURITY_PROPERTIES_FILE, NIFI_BOOTSTRAP_CONF, NIFI_CONFIG_DIRECTORY,
8282
NIFI_PROPERTIES, NIFI_PYTHON_WORKING_DIRECTORY, NIFI_STATE_MANAGEMENT_XML, NifiRepository,
8383
build_bootstrap_conf, build_nifi_properties, build_state_management_xml,
84-
validated_product_config,
8584
},
8685
crd::{
8786
APP_NAME, BALANCE_PORT, BALANCE_PORT_NAME, Container, HTTPS_PORT, HTTPS_PORT_NAME,
8887
METRICS_PORT, METRICS_PORT_NAME, NifiConfig, NifiNodeRoleConfig, NifiRole, NifiRoleType,
8988
NifiStatus, PROTOCOL_PORT, PROTOCOL_PORT_NAME, STACKABLE_LOG_CONFIG_DIR, STACKABLE_LOG_DIR,
90-
authentication::AuthenticationClassResolved, authorization::NifiAccessPolicyProvider,
91-
v1alpha1,
89+
authorization::NifiAccessPolicyProvider, v1alpha1,
9290
},
9391
listener::{
9492
LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build_group_listener, build_group_listener_pvc,
@@ -100,7 +98,7 @@ use crate::{
10098
upgrade::{self, ClusterVersionUpdateState},
10199
},
102100
product_logging::extend_role_group_config_map,
103-
reporting_task::{self, build_maybe_reporting_task, build_reporting_task_service_name},
101+
reporting_task::{build_maybe_reporting_task, build_reporting_task_service_name},
104102
security::{
105103
authentication::{
106104
AUTHORIZERS_XML_FILE_NAME, LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME,
@@ -143,6 +141,12 @@ pub enum Error {
143141
#[snafu(display("object defines no namespace"))]
144142
ObjectHasNoNamespace,
145143

144+
#[snafu(display("failed to dereference resources"))]
145+
Dereference { source: dereference::Error },
146+
147+
#[snafu(display("failed to validate cluster"))]
148+
ValidateCluster { source: validate::Error },
149+
146150
#[snafu(display("failed to create cluster resources"))]
147151
CreateClusterResources {
148152
source: stackable_operator::cluster_resources::Error,
@@ -200,12 +204,6 @@ pub enum Error {
200204
source: stackable_operator::builder::meta::Error,
201205
},
202206

203-
#[snafu(display("Failed to load product config"))]
204-
ProductConfigLoadFailed {
205-
#[snafu(source(from(config::Error, Box::new)))]
206-
source: Box<config::Error>,
207-
},
208-
209207
#[snafu(display("Failed to find information about file [{}] in product config", kind))]
210208
ProductConfigKindNotSpecified { kind: String },
211209

@@ -277,11 +275,6 @@ pub enum Error {
277275
source: crate::security::authorization::Error,
278276
},
279277

280-
#[snafu(display("Failed to resolve NiFi Authentication Configuration"))]
281-
FailedResolveNifiAuthenticationConfig {
282-
source: crate::crd::authentication::Error,
283-
},
284-
285278
#[snafu(display("failed to create PodDisruptionBudget"))]
286279
FailedToCreatePdb {
287280
source: crate::operations::pdb::Error,
@@ -348,11 +341,6 @@ pub enum Error {
348341

349342
#[snafu(display("failed to build authorization configuration"))]
350343
AuthorizationConfiguration { source: authorization::Error },
351-
352-
#[snafu(display("failed to resolve product image"))]
353-
ResolveProductImage {
354-
source: product_image_selection::Error,
355-
},
356344
}
357345

358346
type Result<T, E = Error> = std::result::Result<T, E>;
@@ -376,15 +364,25 @@ pub async fn reconcile_nifi(
376364

377365
let client = &ctx.client;
378366

379-
let resolved_product_image = nifi
380-
.spec
381-
.image
382-
.resolve(
383-
CONTAINER_IMAGE_BASE_NAME,
384-
&ctx.operator_environment.image_repository,
385-
crate::built_info::PKG_VERSION,
386-
)
387-
.context(ResolveProductImageSnafu)?;
367+
// dereference (client required)
368+
let dereferenced_objects = dereference::dereference(client, nifi)
369+
.await
370+
.context(DereferenceSnafu)?;
371+
372+
// validate (no client required)
373+
let validated = validate::validate(
374+
nifi,
375+
&dereferenced_objects,
376+
&ctx.operator_environment,
377+
&ctx.product_config,
378+
)
379+
.context(ValidateClusterSnafu)?;
380+
381+
let resolved_product_image = validated.image;
382+
let authentication_config = validated.authentication_config;
383+
let authorization_config = validated.authorization_config;
384+
let validated_config = validated.validated_role_config;
385+
let proxy_hosts = dereferenced_objects.proxy_hosts;
388386

389387
tracing::info!("Checking for sensitive key configuration");
390388
check_or_generate_sensitive_key(client, nifi)
@@ -419,14 +417,6 @@ pub async fn reconcile_nifi(
419417
}
420418
// end todo
421419

422-
let validated_config = validated_product_config(
423-
nifi,
424-
&resolved_product_image.product_version,
425-
nifi.spec.nodes.as_ref().context(NoNodesDefinedSnafu)?,
426-
&ctx.product_config,
427-
)
428-
.context(ProductConfigLoadFailedSnafu)?;
429-
430420
let mut cluster_resources = ClusterResources::new(
431421
APP_NAME,
432422
OPERATOR_NAME,
@@ -442,30 +432,12 @@ pub async fn reconcile_nifi(
442432
.map(Cow::Borrowed)
443433
.unwrap_or_default();
444434

445-
let authentication_config = NifiAuthenticationConfig::try_from(
446-
AuthenticationClassResolved::from(nifi, client)
447-
.await
448-
.context(FailedResolveNifiAuthenticationConfigSnafu)?,
449-
)
450-
.context(InvalidNifiAuthenticationConfigSnafu)?;
451-
452435
if let NifiAuthenticationConfig::Oidc { .. } = authentication_config {
453436
check_or_generate_oidc_admin_password(client, nifi)
454437
.await
455438
.context(SecuritySnafu)?;
456439
}
457440

458-
let authorization_config = ResolvedNifiAuthorizationConfig::from(
459-
&nifi.spec.cluster_config.authorization,
460-
client,
461-
nifi.metadata
462-
.namespace
463-
.as_deref()
464-
.context(ObjectHasNoNamespaceSnafu)?,
465-
)
466-
.await
467-
.context(InvalidNifiAuthorizationConfigSnafu)?;
468-
469441
let (rbac_sa, rbac_rolebinding) = build_rbac_resources(
470442
nifi,
471443
APP_NAME,
@@ -530,13 +502,9 @@ pub async fn reconcile_nifi(
530502

531503
let role = nifi.spec.nodes.as_ref().context(NoNodesDefinedSnafu)?;
532504

533-
// This is due to the fact that users might access NiFi via these addresses, if they try to
534-
// connect from an external machine (not inside the k8s overlay network).
535-
// Since we cannot predict which of the addresses a user might decide to use we will simply
536-
// add all of them to the setting for now.
505+
// The proxy hosts allow-list lets external users access NiFi via addresses we cannot
506+
// predict, so all of them are added to the setting.
537507
// For more information see <https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#proxy_configuration>
538-
let proxy_hosts = get_proxy_hosts(client, nifi, &resolved_product_image).await?;
539-
540508
let rg_configmap = build_node_rolegroup_config_map(
541509
nifi,
542510
&resolved_product_image,
@@ -738,9 +706,7 @@ async fn build_node_rolegroup_config_map(
738706
.get_authentication_config()
739707
.context(InvalidNifiAuthenticationConfigSnafu)?;
740708

741-
let authorizers_xml = authorization_config
742-
.get_authorizers_config(nifi)
743-
.context(InvalidNifiAuthorizationConfigSnafu)?;
709+
let authorizers_xml = authorization_config.get_authorizers_config(nifi);
744710

745711
let jvm_sec_props: BTreeMap<String, Option<String>> = rolegroup_config
746712
.get(&PropertyNameKind::File(
@@ -1534,48 +1500,6 @@ fn get_volume_claim_templates(
15341500
Ok(pvcs)
15351501
}
15361502

1537-
async fn get_proxy_hosts(
1538-
client: &Client,
1539-
nifi: &v1alpha1::NifiCluster,
1540-
resolved_product_image: &ResolvedProductImage,
1541-
) -> Result<String> {
1542-
let host_header_check = nifi.spec.cluster_config.host_header_check.clone();
1543-
1544-
if host_header_check.allow_all {
1545-
tracing::info!(
1546-
"spec.clusterConfig.hostHeaderCheck.allowAll is set to true. All proxy hosts will be allowed."
1547-
);
1548-
if !host_header_check.additional_allowed_hosts.is_empty() {
1549-
tracing::info!(
1550-
"spec.clusterConfig.hostHeaderCheck.additionalAllowedHosts is ignored and only '*' is added to the allow-list."
1551-
)
1552-
}
1553-
return Ok("*".to_string());
1554-
}
1555-
1556-
// Address and port are injected from the listener volume during the prepare container
1557-
let mut proxy_hosts = HashSet::from([
1558-
"${env:LISTENER_DEFAULT_ADDRESS}:${env:LISTENER_DEFAULT_PORT_HTTPS}".to_string(),
1559-
]);
1560-
proxy_hosts.extend(host_header_check.additional_allowed_hosts);
1561-
1562-
// Reporting task only exists for NiFi 1.x
1563-
if resolved_product_image.product_version.starts_with("1.") {
1564-
let reporting_task_service_name = reporting_task::build_reporting_task_fqdn_service_name(
1565-
nifi,
1566-
&client.kubernetes_cluster_info,
1567-
)
1568-
.context(ReportingTaskSnafu)?;
1569-
1570-
proxy_hosts.insert(format!("{reporting_task_service_name}:{HTTPS_PORT}"));
1571-
}
1572-
1573-
let mut proxy_hosts = Vec::from_iter(proxy_hosts);
1574-
proxy_hosts.sort();
1575-
1576-
Ok(proxy_hosts.join(","))
1577-
}
1578-
15791503
pub fn error_policy(
15801504
_obj: Arc<DeserializeGuard<v1alpha1::NifiCluster>>,
15811505
error: &Error,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! The dereference step in the NifiCluster controller
2+
//!
3+
//! Fetches all Kubernetes objects referenced by the NifiCluster spec and returns
4+
//! them in [`DereferencedObjects`].
5+
6+
use std::collections::HashSet;
7+
8+
use snafu::{OptionExt, ResultExt, Snafu};
9+
use stackable_operator::client::Client;
10+
11+
use crate::{
12+
crd::{HTTPS_PORT, v1alpha1},
13+
reporting_task,
14+
security::{
15+
authentication::{self, DereferencedAuthenticationClasses},
16+
authorization::{self as authorization_mod, DereferencedAuthorization},
17+
},
18+
};
19+
20+
#[derive(Snafu, Debug)]
21+
pub enum Error {
22+
#[snafu(display("object defines no namespace"))]
23+
ObjectHasNoNamespace,
24+
25+
#[snafu(display("failed to dereference NiFi authentication classes"))]
26+
DereferenceAuthenticationClasses { source: authentication::Error },
27+
28+
#[snafu(display("failed to dereference NiFi authorization config"))]
29+
DereferenceAuthorization { source: authorization_mod::Error },
30+
31+
#[snafu(display("failed to build reporting task service name"))]
32+
ReportingTask {
33+
source: crate::reporting_task::Error,
34+
},
35+
}
36+
37+
type Result<T, E = Error> = std::result::Result<T, E>;
38+
39+
/// Kubernetes objects referenced from the [`v1alpha1::NifiCluster`] spec, already fetched.
40+
pub struct DereferencedObjects {
41+
pub authentication_classes: DereferencedAuthenticationClasses,
42+
pub authorization: DereferencedAuthorization,
43+
/// Comma-separated NiFi proxy hosts, or `"*"` if
44+
/// `spec.clusterConfig.hostHeaderCheck.allowAll` is set. Computed here so all remote
45+
/// lookups live in one place; previously this ran once per rolegroup inside the
46+
/// reconcile loop.
47+
pub proxy_hosts: String,
48+
}
49+
50+
/// Fetches all Kubernetes objects referenced from the [`v1alpha1::NifiCluster`] spec.
51+
pub async fn dereference(
52+
client: &Client,
53+
nifi: &v1alpha1::NifiCluster,
54+
) -> Result<DereferencedObjects> {
55+
let namespace = nifi
56+
.metadata
57+
.namespace
58+
.as_deref()
59+
.context(ObjectHasNoNamespaceSnafu)?;
60+
61+
let authentication_classes = DereferencedAuthenticationClasses::dereference(nifi, client)
62+
.await
63+
.context(DereferenceAuthenticationClassesSnafu)?;
64+
65+
let authorization = DereferencedAuthorization::dereference(
66+
&nifi.spec.cluster_config.authorization,
67+
client,
68+
namespace,
69+
)
70+
.await
71+
.context(DereferenceAuthorizationSnafu)?;
72+
73+
let proxy_hosts = compute_proxy_hosts(client, nifi)?;
74+
75+
Ok(DereferencedObjects {
76+
authentication_classes,
77+
authorization,
78+
proxy_hosts,
79+
})
80+
}
81+
82+
fn compute_proxy_hosts(client: &Client, nifi: &v1alpha1::NifiCluster) -> Result<String> {
83+
let host_header_check = &nifi.spec.cluster_config.host_header_check;
84+
85+
if host_header_check.allow_all {
86+
tracing::info!(
87+
"spec.clusterConfig.hostHeaderCheck.allowAll is set to true. All proxy hosts will be allowed."
88+
);
89+
if !host_header_check.additional_allowed_hosts.is_empty() {
90+
tracing::info!(
91+
"spec.clusterConfig.hostHeaderCheck.additionalAllowedHosts is ignored and only '*' is added to the allow-list."
92+
)
93+
}
94+
return Ok("*".to_string());
95+
}
96+
97+
// Address and port are injected from the listener volume during the prepare container
98+
let mut proxy_hosts = HashSet::from([
99+
"${env:LISTENER_DEFAULT_ADDRESS}:${env:LISTENER_DEFAULT_PORT_HTTPS}".to_string(),
100+
]);
101+
proxy_hosts.extend(host_header_check.additional_allowed_hosts.iter().cloned());
102+
103+
// Reporting task only exists for NiFi 1.x
104+
if nifi.spec.image.product_version().starts_with("1.") {
105+
let reporting_task_service_name = reporting_task::build_reporting_task_fqdn_service_name(
106+
nifi,
107+
&client.kubernetes_cluster_info,
108+
)
109+
.context(ReportingTaskSnafu)?;
110+
111+
proxy_hosts.insert(format!("{reporting_task_service_name}:{HTTPS_PORT}"));
112+
}
113+
114+
let mut proxy_hosts = Vec::from_iter(proxy_hosts);
115+
proxy_hosts.sort();
116+
117+
Ok(proxy_hosts.join(","))
118+
}

0 commit comments

Comments
 (0)