11use std:: fmt;
22
3+ #[ cfg( doc) ]
4+ use { k8s_openapi:: api:: core:: v1:: PodSpec , std:: collections:: BTreeMap } ;
5+
6+ use indexmap:: IndexMap ;
37use k8s_openapi:: api:: core:: v1:: {
48 ConfigMapKeySelector , Container , ContainerPort , EnvVar , EnvVarSource , Lifecycle ,
59 LifecycleHandler , ObjectFieldSelector , Probe , ResourceRequirements , SecretKeySelector ,
610 SecurityContext , VolumeMount ,
711} ;
812use snafu:: { ResultExt as _, Snafu } ;
13+ use tracing:: instrument;
914
1015use crate :: {
1116 commons:: product_image_selection:: ResolvedProductImage ,
@@ -21,11 +26,26 @@ pub enum Error {
2126 source : validation:: Errors ,
2227 container_name : String ,
2328 } ,
29+
30+ #[ snafu( display(
31+ "Colliding mountPath {mount_path:?} in volumeMounts with different content. \
32+ Existing volume name {existing_volume_name:?}, new volume name {new_volume_name:?}"
33+ ) ) ]
34+ MountPathCollision {
35+ mount_path : String ,
36+ existing_volume_name : String ,
37+ new_volume_name : String ,
38+ } ,
2439}
2540
2641/// A builder to build [`Container`] objects.
2742///
2843/// This will automatically create the necessary volumes and mounts for each `ConfigMap` which is added.
44+ ///
45+ /// This struct is often times using an [`IndexMap`] to have consistent ordering (so we don't produce reconcile loops).
46+ /// We are also choosing it over a [`BTreeMap`], because it is easier to debug for users, as logically
47+ /// grouped volumeMounts (e.g. all volumeMounts related to S3) are near each other in the list instead of "just" being
48+ /// sorted alphabetically.
2949#[ derive( Clone , Default ) ]
3050pub struct ContainerBuilder {
3151 args : Option < Vec < String > > ,
@@ -36,7 +56,9 @@ pub struct ContainerBuilder {
3656 image_pull_policy : Option < String > ,
3757 name : String ,
3858 resources : Option < ResourceRequirements > ,
39- volume_mounts : Option < Vec < VolumeMount > > ,
59+
60+ /// The key is the volumeMount mountPath.
61+ volume_mounts : IndexMap < String , VolumeMount > ,
4062 readiness_probe : Option < Probe > ,
4163 liveness_probe : Option < Probe > ,
4264 startup_probe : Option < Probe > ,
@@ -188,29 +210,79 @@ impl ContainerBuilder {
188210 self
189211 }
190212
213+ /// Adds a new [`VolumeMount`] to the container while ensuring that no colliding [`VolumeMount`]
214+ /// exists.
215+ ///
216+ /// A colliding [`VolumeMount`] would have the same mountPath but a different content than
217+ /// another [`VolumeMount`]. An appropriate error is returned when such a colliding mount path is
218+ /// encountered.
219+ ///
220+ /// ### Note
221+ ///
222+ /// Previously, this function unconditionally added [`VolumeMount`]s, which resulted in invalid
223+ /// [`PodSpec`]s.
224+ #[ instrument( skip( self ) ) ]
225+ fn add_volume_mount_impl ( & mut self , volume_mount : VolumeMount ) -> Result < & mut Self > {
226+ if let Some ( existing_volume_mount) = self . volume_mounts . get ( & volume_mount. mount_path ) {
227+ if existing_volume_mount != & volume_mount {
228+ let colliding_mount_path = & volume_mount. mount_path ;
229+ // We don't want to include the details in the error message, but instead trace them
230+ tracing:: error!(
231+ colliding_mount_path,
232+ ?existing_volume_mount,
233+ "Colliding mountPath {colliding_mount_path:?} in volumeMounts with different content"
234+ ) ;
235+ }
236+ MountPathCollisionSnafu {
237+ mount_path : & volume_mount. mount_path ,
238+ existing_volume_name : & existing_volume_mount. name ,
239+ new_volume_name : & volume_mount. name ,
240+ }
241+ . fail ( ) ?;
242+ } else {
243+ self . volume_mounts
244+ . insert ( volume_mount. mount_path . clone ( ) , volume_mount) ;
245+ }
246+
247+ Ok ( self )
248+ }
249+
250+ /// Adds a new [`VolumeMount`] to the container while ensuring that no colliding [`VolumeMount`]
251+ /// exists.
252+ ///
253+ /// A colliding [`VolumeMount`] would have the same mountPath but a different content than
254+ /// another [`VolumeMount`]. An appropriate error is returned when such a colliding mount path is
255+ /// encountered.
256+ ///
257+ /// ### Note
258+ ///
259+ /// Previously, this function unconditionally added [`VolumeMount`]s, which resulted in invalid
260+ /// [`PodSpec`]s.
191261 pub fn add_volume_mount (
192262 & mut self ,
193263 name : impl Into < String > ,
194264 path : impl Into < String > ,
195- ) -> & mut Self {
196- self . volume_mounts
197- . get_or_insert_with ( Vec :: new)
198- . push ( VolumeMount {
199- name : name. into ( ) ,
200- mount_path : path. into ( ) ,
201- ..VolumeMount :: default ( )
202- } ) ;
203- self
265+ ) -> Result < & mut Self > {
266+ self . add_volume_mount_impl ( VolumeMount {
267+ name : name. into ( ) ,
268+ mount_path : path. into ( ) ,
269+ ..VolumeMount :: default ( )
270+ } )
204271 }
205272
273+ /// Adds new [`VolumeMount`]s to the container while ensuring that no colliding [`VolumeMount`]
274+ /// exists.
275+ ///
276+ /// See [`Self::add_volume_mount`] for details.
206277 pub fn add_volume_mounts (
207278 & mut self ,
208279 volume_mounts : impl IntoIterator < Item = VolumeMount > ,
209- ) -> & mut Self {
210- self . volume_mounts
211- . get_or_insert_with ( Vec :: new)
212- . extend ( volume_mounts) ;
213- self
280+ ) -> Result < & mut Self > {
281+ for volume_mount in volume_mounts {
282+ self . add_volume_mount_impl ( volume_mount) ?;
283+ }
284+
285+ Ok ( self )
214286 }
215287
216288 pub fn readiness_probe ( & mut self , probe : Probe ) -> & mut Self {
@@ -256,6 +328,12 @@ impl ContainerBuilder {
256328 }
257329
258330 pub fn build ( & self ) -> Container {
331+ let volume_mounts = if self . volume_mounts . is_empty ( ) {
332+ None
333+ } else {
334+ Some ( self . volume_mounts . values ( ) . cloned ( ) . collect ( ) )
335+ } ;
336+
259337 Container {
260338 args : self . args . clone ( ) ,
261339 command : self . command . clone ( ) ,
@@ -265,7 +343,7 @@ impl ContainerBuilder {
265343 resources : self . resources . clone ( ) ,
266344 name : self . name . clone ( ) ,
267345 ports : self . container_ports . clone ( ) ,
268- volume_mounts : self . volume_mounts . clone ( ) ,
346+ volume_mounts,
269347 readiness_probe : self . readiness_probe . clone ( ) ,
270348 liveness_probe : self . liveness_probe . clone ( ) ,
271349 startup_probe : self . startup_probe . clone ( ) ,
@@ -388,6 +466,7 @@ mod tests {
388466 . add_env_var_from_config_map ( "envFromConfigMap" , "my-configmap" , "my-key" )
389467 . add_env_var_from_secret ( "envFromSecret" , "my-secret" , "my-key" )
390468 . add_volume_mount ( "configmap" , "/mount" )
469+ . expect ( "add volume mount" )
391470 . add_container_port ( container_port_name, container_port)
392471 . resources ( resources. clone ( ) )
393472 . add_container_ports ( vec ! [ ContainerPortBuilder :: new( container_port_1)
@@ -491,20 +570,18 @@ mod tests {
491570 "lengthexceededlengthexceededlengthexceededlengthexceededlengthex" ;
492571 assert_eq ! ( long_container_name. len( ) , 64 ) ; // 63 characters is the limit for container names
493572 let result = ContainerBuilder :: new ( long_container_name) ;
494- match result
573+ if let Error :: InvalidContainerName {
574+ container_name,
575+ source,
576+ } = result
495577 . err ( )
496578 . expect ( "Container name exceeding 63 characters should cause an error" )
497579 {
498- Error :: InvalidContainerName {
499- container_name,
500- source,
501- } => {
502- assert_eq ! ( container_name, long_container_name) ;
503- assert_eq ! (
504- source. to_string( ) ,
505- "input is 64 bytes long but must be no more than 63"
506- )
507- }
580+ assert_eq ! ( container_name, long_container_name) ;
581+ assert_eq ! (
582+ source. to_string( ) ,
583+ "input is 64 bytes long but must be no more than 63"
584+ )
508585 }
509586 // One characters shorter name is valid
510587 let max_len_container_name: String = long_container_name. chars ( ) . skip ( 1 ) . collect ( ) ;
@@ -568,16 +645,14 @@ mod tests {
568645 result : Result < ContainerBuilder , Error > ,
569646 expected_err_contains : & str ,
570647 ) {
571- match result
648+ if let Error :: InvalidContainerName {
649+ container_name : _,
650+ source,
651+ } = result
572652 . err ( )
573653 . expect ( "Container name exceeding 63 characters should cause an error" )
574654 {
575- Error :: InvalidContainerName {
576- container_name : _,
577- source,
578- } => {
579- assert ! ( dbg!( source. to_string( ) ) . contains( dbg!( expected_err_contains) ) ) ;
580- }
655+ assert ! ( dbg!( source. to_string( ) ) . contains( dbg!( expected_err_contains) ) ) ;
581656 }
582657 }
583658}
0 commit comments