Skip to content

Commit 160a97f

Browse files
authored
refactor: Move user-info-fetcher sidecar creation into separate module (#862)
1 parent 8e8f19a commit 160a97f

2 files changed

Lines changed: 361 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: 156 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,
27+
ResourceRequirements,
3428
},
3529
},
3630
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
@@ -53,11 +47,16 @@ use stackable_operator::{
5347

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

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

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

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

173150
type Result<T, E = Error> = std::result::Result<T, E>;
@@ -437,129 +414,14 @@ pub fn build_server_rolegroup_daemonset(
437414
.context(AddVolumeSnafu)?;
438415
}
439416

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

564426
// The Vector logging config was validated up-front (see `ValidatedLogging`); a `Some` here means
565427
// the Vector agent is enabled and the aggregator discovery ConfigMap name is valid.
@@ -846,7 +708,9 @@ fn build_prepare_start_command(
846708
#[cfg(test)]
847709
mod tests {
848710
use serde_json::json;
849-
use stackable_operator::commons::networking::DomainName;
711+
use stackable_operator::{
712+
commons::networking::DomainName, k8s_openapi::api::core::v1::Container,
713+
};
850714

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

0 commit comments

Comments
 (0)