@@ -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
5549use super :: service:: { self , APP_PORT , APP_PORT_NAME } ;
5650use 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+
6261pub const BUNDLES_ACTIVE_DIR : & str = "/bundles/active" ;
6362pub const BUNDLES_INCOMING_DIR : & str = "/bundles/incoming" ;
6463pub 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
174151type 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