@@ -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
5448use super :: service:: { self , APP_PORT , APP_PORT_NAME } ;
5549use 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+
6160pub const BUNDLES_ACTIVE_DIR : & str = "/bundles/active" ;
6261pub const BUNDLES_INCOMING_DIR : & str = "/bundles/incoming" ;
6362pub 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
173150type 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) ]
847709mod 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