Skip to content

Commit a25da2b

Browse files
committed
refactor: Move user-info-fetcher sidecar creation into separate module
1 parent 5b3aa4c commit a25da2b

2 files changed

Lines changed: 360 additions & 159 deletions

File tree

rust/operator-binary/src/controller/build/resource/daemonset.rs renamed to rust/operator-binary/src/controller/build/resource/daemonset/mod.rs

Lines changed: 155 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,14 @@ use stackable_operator::{
1717
volume::{SecretOperatorVolumeSourceBuilder, VolumeBuilder},
1818
},
1919
},
20-
commons::{
21-
secret_class::{
22-
SecretClassVolume, SecretClassVolumeProvisionParts, SecretClassVolumeScope,
23-
},
24-
tls_verification::{TlsClientDetails, TlsClientDetailsError},
25-
},
26-
crd::authentication::ldap,
20+
commons::secret_class::SecretClassVolumeProvisionParts,
2721
k8s_openapi::{
2822
DeepMerge,
2923
api::{
3024
apps::v1::{DaemonSet, DaemonSetSpec, DaemonSetUpdateStrategy, RollingUpdateDaemonSet},
3125
core::v1::{
3226
EmptyDirVolumeSource, EnvVarSource, HTTPGetAction, ObjectFieldSelector, Probe,
33-
ResourceRequirements, SecretVolumeSource, ServiceAccount,
27+
ResourceRequirements, ServiceAccount,
3428
},
3529
},
3630
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
@@ -54,11 +48,16 @@ use stackable_operator::{
5448

5549
use super::service::{self, APP_PORT, APP_PORT_NAME};
5650
use crate::{
57-
controller::{OpaRoleGroupConfig, RoleGroupName, ValidatedCluster, ValidatedOpaConfig, build},
58-
crd::{Container, DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT, user_info_fetcher},
51+
controller::{
52+
OpaRoleGroupConfig, RoleGroupName, ValidatedCluster, ValidatedOpaConfig,
53+
build::{self, resource::daemonset::user_info_fetcher::add_user_info_fetcher_sidecar},
54+
},
55+
crd::{Container, DEFAULT_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT},
5956
operations::graceful_shutdown::add_graceful_shutdown_config,
6057
};
6158

59+
mod user_info_fetcher;
60+
6261
pub const BUNDLES_ACTIVE_DIR: &str = "/bundles/active";
6362
pub const BUNDLES_INCOMING_DIR: &str = "/bundles/incoming";
6463
pub const BUNDLES_TMP_DIR: &str = "/bundles/tmp";
@@ -132,31 +131,6 @@ pub enum Error {
132131
source: crate::operations::graceful_shutdown::Error,
133132
},
134133

135-
#[snafu(display("failed to build volume spec for the User Info Fetcher TLS config"))]
136-
UserInfoFetcherKerberosVolume {
137-
source: stackable_operator::builder::pod::Error,
138-
},
139-
140-
#[snafu(display("failed to build volume mount spec for the User Info Fetcher TLS config"))]
141-
UserInfoFetcherKerberosVolumeMount {
142-
source: stackable_operator::builder::pod::container::Error,
143-
},
144-
145-
#[snafu(display("failed to convert the User Info Fetcher Kerberos SecretClass into a volume"))]
146-
ConvertUserInfoFetcherKerberosSecretClassVolume {
147-
source: stackable_operator::commons::secret_class::SecretClassVolumeError,
148-
},
149-
150-
#[snafu(display(
151-
"failed to build volume or volume mount spec for the User Info Fetcher TLS config"
152-
))]
153-
UserInfoFetcherTlsVolumeAndMounts { source: TlsClientDetailsError },
154-
155-
#[snafu(display(
156-
"failed to build volume or volume mount spec for the User Info Fetcher LDAP config"
157-
))]
158-
UserInfoFetcherLdapVolumeAndMounts { source: ldap::v1alpha1::Error },
159-
160134
#[snafu(display("failed to add needed volume"))]
161135
AddVolume { source: builder::pod::Error },
162136

@@ -169,6 +143,9 @@ pub enum Error {
169143
TlsVolumeBuild {
170144
source: builder::pod::volume::SecretOperatorVolumeSourceBuilderError,
171145
},
146+
147+
#[snafu(display("failed to build User Info Fetcher sidecar"))]
148+
BuildUserInfoFetcherSidecar { source: user_info_fetcher::Error },
172149
}
173150

174151
type Result<T, E = Error> = std::result::Result<T, E>;
@@ -438,129 +415,14 @@ pub fn build_server_rolegroup_daemonset(
438415
.context(AddVolumeSnafu)?;
439416
}
440417

441-
if let Some(user_info) = &cluster.cluster_config.user_info {
442-
let user_info_fetcher_container_name = container_name(&Container::UserInfoFetcher);
443-
let mut cb_user_info_fetcher = new_container_builder(&user_info_fetcher_container_name);
444-
445-
cb_user_info_fetcher
446-
.image_from_product_image(resolved_product_image) // inherit the pull policy and pull secrets, and then...
447-
.image(user_info_fetcher_image) // ...override the image
448-
.command(vec!["stackable-opa-user-info-fetcher".to_string()])
449-
.add_env_var(
450-
"CONFIG",
451-
format!(
452-
"{CONFIG_DIR}/{file}",
453-
file = build::properties::ConfigFileName::UserInfoFetcher
454-
),
455-
)
456-
.add_env_var("CREDENTIALS_DIR", USER_INFO_FETCHER_CREDENTIALS_DIR)
457-
.add_volume_mount(CONFIG_VOLUME_NAME.as_ref(), CONFIG_DIR)
458-
.context(AddVolumeMountSnafu)?
459-
.resources(sidecar_resource_requirements());
460-
add_stackable_rust_cli_env_vars(
461-
&mut cb_user_info_fetcher,
462-
cluster_info,
463-
sidecar_container_log_level(merged_config, &Container::UserInfoFetcher).to_string(),
464-
&Container::UserInfoFetcher,
465-
);
466-
467-
match &user_info.backend {
468-
user_info_fetcher::v1alpha2::Backend::None {} => {}
469-
user_info_fetcher::v1alpha2::Backend::ExperimentalXfscAas(_) => {}
470-
user_info_fetcher::v1alpha2::Backend::ActiveDirectory(ad) => {
471-
pb.add_volume(
472-
SecretClassVolume::new(
473-
ad.kerberos_secret_class_name.to_string(),
474-
Some(SecretClassVolumeScope {
475-
pod: false,
476-
node: false,
477-
services: vec![cluster.name.to_string()],
478-
listener_volumes: Vec::new(),
479-
}),
480-
)
481-
.to_volume(
482-
USER_INFO_FETCHER_KERBEROS_VOLUME_NAME.as_ref(),
483-
// The user-info-fetcher needs both the keytab (private) and the Kerberos config (public).
484-
SecretClassVolumeProvisionParts::PublicPrivate,
485-
)
486-
.context(ConvertUserInfoFetcherKerberosSecretClassVolumeSnafu)?,
487-
)
488-
.context(UserInfoFetcherKerberosVolumeSnafu)?;
489-
cb_user_info_fetcher
490-
.add_volume_mount(
491-
USER_INFO_FETCHER_KERBEROS_VOLUME_NAME.as_ref(),
492-
USER_INFO_FETCHER_KERBEROS_DIR,
493-
)
494-
.context(UserInfoFetcherKerberosVolumeMountSnafu)?;
495-
cb_user_info_fetcher.add_env_var(
496-
"KRB5_CONFIG",
497-
format!("{USER_INFO_FETCHER_KERBEROS_DIR}/krb5.conf"),
498-
);
499-
cb_user_info_fetcher.add_env_var(
500-
"KRB5_CLIENT_KTNAME",
501-
format!("{USER_INFO_FETCHER_KERBEROS_DIR}/keytab"),
502-
);
503-
cb_user_info_fetcher.add_env_var("KRB5CCNAME", "MEMORY:".to_string());
504-
ad.tls
505-
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
506-
.context(UserInfoFetcherTlsVolumeAndMountsSnafu)?;
507-
}
508-
user_info_fetcher::v1alpha2::Backend::Keycloak(keycloak) => {
509-
pb.add_volume(
510-
VolumeBuilder::new(USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME.as_ref())
511-
.secret(SecretVolumeSource {
512-
secret_name: Some(keycloak.client_credentials_secret.to_string()),
513-
..Default::default()
514-
})
515-
.build(),
516-
)
517-
.context(AddVolumeSnafu)?;
518-
cb_user_info_fetcher
519-
.add_volume_mount(
520-
USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME.as_ref(),
521-
USER_INFO_FETCHER_CREDENTIALS_DIR,
522-
)
523-
.context(AddVolumeMountSnafu)?;
524-
keycloak
525-
.tls
526-
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
527-
.context(UserInfoFetcherTlsVolumeAndMountsSnafu)?;
528-
}
529-
user_info_fetcher::v1alpha2::Backend::Entra(entra) => {
530-
pb.add_volume(
531-
VolumeBuilder::new(USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME.as_ref())
532-
.secret(SecretVolumeSource {
533-
secret_name: Some(entra.client_credentials_secret.to_string()),
534-
..Default::default()
535-
})
536-
.build(),
537-
)
538-
.context(AddVolumeSnafu)?;
539-
cb_user_info_fetcher
540-
.add_volume_mount(
541-
USER_INFO_FETCHER_CREDENTIALS_VOLUME_NAME.as_ref(),
542-
USER_INFO_FETCHER_CREDENTIALS_DIR,
543-
)
544-
.context(AddVolumeMountSnafu)?;
545-
546-
TlsClientDetails {
547-
tls: entra.tls.clone(),
548-
}
549-
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
550-
.context(UserInfoFetcherTlsVolumeAndMountsSnafu)?;
551-
}
552-
user_info_fetcher::v1alpha2::Backend::OpenLdap(openldap) => {
553-
// Reuse the logic from the LDAP `AuthenticationProvider` which handles
554-
// volume mounting of TLS secrets and LDAP bind credentials
555-
openldap
556-
.to_ldap_provider()
557-
.add_volumes_and_mounts(&mut pb, vec![&mut cb_user_info_fetcher])
558-
.context(UserInfoFetcherLdapVolumeAndMountsSnafu)?;
559-
}
560-
}
561-
562-
pb.add_container(cb_user_info_fetcher.build());
563-
}
418+
add_user_info_fetcher_sidecar(
419+
&mut pb,
420+
cluster,
421+
merged_config,
422+
user_info_fetcher_image,
423+
cluster_info,
424+
)
425+
.context(BuildUserInfoFetcherSidecarSnafu)?;
564426

565427
// The Vector logging config was validated up-front (see `ValidatedLogging`); a `Some` here means
566428
// the Vector agent is enabled and the aggregator discovery ConfigMap name is valid.
@@ -850,7 +712,8 @@ mod tests {
850712
use stackable_operator::{
851713
commons::networking::DomainName,
852714
k8s_openapi::{
853-
api::core::v1::ServiceAccount, apimachinery::pkg::apis::meta::v1::ObjectMeta,
715+
api::core::v1::{Container, ServiceAccount},
716+
apimachinery::pkg::apis::meta::v1::ObjectMeta,
854717
},
855718
};
856719

@@ -1128,4 +991,137 @@ mod tests {
1128991
.contains("stackable-opa-bundle-builder &")
1129992
);
1130993
}
994+
995+
fn uif_container(ds: &DaemonSet) -> Container {
996+
ds.spec
997+
.as_ref()
998+
.unwrap()
999+
.template
1000+
.spec
1001+
.as_ref()
1002+
.unwrap()
1003+
.containers
1004+
.iter()
1005+
.find(|c| c.name == "user-info-fetcher")
1006+
.expect("the user-info-fetcher container should exist")
1007+
.clone()
1008+
}
1009+
1010+
fn env_var(container: &Container, name: &str) -> String {
1011+
container
1012+
.env
1013+
.as_ref()
1014+
.expect("the container should have env vars")
1015+
.iter()
1016+
.find(|e| e.name == name)
1017+
.unwrap_or_else(|| panic!("env var {name} should be set"))
1018+
.value
1019+
.clone()
1020+
.unwrap_or_else(|| panic!("env var {name} should have a literal value"))
1021+
}
1022+
1023+
fn mount_path(container: &Container, volume_name: &str) -> String {
1024+
container
1025+
.volume_mounts
1026+
.as_ref()
1027+
.expect("the container should have volume mounts")
1028+
.iter()
1029+
.find(|m| m.name == volume_name)
1030+
.unwrap_or_else(|| panic!("volume mount {volume_name} should exist"))
1031+
.mount_path
1032+
.clone()
1033+
}
1034+
1035+
#[test]
1036+
fn user_info_fetcher_container_has_expected_command_and_config_wiring() {
1037+
let ds = build(&validated_cluster_from_spec(json!({
1038+
"image": { "productVersion": "1.2.3" },
1039+
"clusterConfig": {
1040+
"userInfo": {
1041+
"backend": {
1042+
"experimentalXfscAas": {
1043+
"hostname": "aas.default.svc.cluster.local",
1044+
"port": 5000,
1045+
}
1046+
}
1047+
}
1048+
},
1049+
"servers": { "roleGroups": { "default": {} } },
1050+
})));
1051+
1052+
let uif = uif_container(&ds);
1053+
assert_eq!(
1054+
uif.command,
1055+
Some(vec!["stackable-opa-user-info-fetcher".to_owned()])
1056+
);
1057+
// The sidecar reads its config from the shared config volume, and looks for backend
1058+
// credentials in a fixed directory (populated by the backend-specific arms below).
1059+
assert_eq!(
1060+
env_var(&uif, "CONFIG"),
1061+
"/stackable/config/user-info-fetcher.json"
1062+
);
1063+
assert_eq!(env_var(&uif, "CREDENTIALS_DIR"), "/stackable/credentials");
1064+
assert_eq!(mount_path(&uif, "config"), "/stackable/config");
1065+
}
1066+
1067+
#[test]
1068+
fn user_info_fetcher_active_directory_backend_mounts_kerberos_and_sets_krb5_env() {
1069+
let ds = build(&validated_cluster_from_spec(json!({
1070+
"image": { "productVersion": "1.2.3" },
1071+
"clusterConfig": {
1072+
"userInfo": {
1073+
"backend": {
1074+
"experimentalActiveDirectory": {
1075+
"ldapServer": "ad.example.com",
1076+
"baseDistinguishedName": "dc=example,dc=com",
1077+
"kerberosSecretClassName": "kerberos",
1078+
}
1079+
}
1080+
}
1081+
},
1082+
"servers": { "roleGroups": { "default": {} } },
1083+
})));
1084+
1085+
// A Kerberos secret volume is provisioned and mounted for the sidecar.
1086+
assert!(volume_names(&ds).contains(&"kerberos".to_owned()));
1087+
let uif = uif_container(&ds);
1088+
assert_eq!(mount_path(&uif, "kerberos"), "/stackable/kerberos");
1089+
// The krb5 client must find the config and keytab, and keep tickets in memory only.
1090+
assert_eq!(
1091+
env_var(&uif, "KRB5_CONFIG"),
1092+
"/stackable/kerberos/krb5.conf"
1093+
);
1094+
assert_eq!(
1095+
env_var(&uif, "KRB5_CLIENT_KTNAME"),
1096+
"/stackable/kerberos/keytab"
1097+
);
1098+
assert_eq!(env_var(&uif, "KRB5CCNAME"), "MEMORY:");
1099+
}
1100+
1101+
#[test]
1102+
fn user_info_fetcher_keycloak_backend_mounts_client_credentials_secret() {
1103+
let ds = build(&validated_cluster_from_spec(json!({
1104+
"image": { "productVersion": "1.2.3" },
1105+
"clusterConfig": {
1106+
"userInfo": {
1107+
"backend": {
1108+
"keycloak": {
1109+
"hostname": "keycloak.example.com",
1110+
"clientCredentialsSecret": "keycloak-credentials",
1111+
"adminRealm": "master",
1112+
"userRealm": "my-realm",
1113+
}
1114+
}
1115+
}
1116+
},
1117+
"servers": { "roleGroups": { "default": {} } },
1118+
})));
1119+
1120+
// The client credentials secret is projected into the sidecar's credentials dir.
1121+
assert!(volume_names(&ds).contains(&"credentials".to_owned()));
1122+
assert_eq!(
1123+
mount_path(&uif_container(&ds), "credentials"),
1124+
"/stackable/credentials"
1125+
);
1126+
}
11311127
}

0 commit comments

Comments
 (0)