1
1
use std:: fmt;
2
2
3
+ #[ cfg( doc) ]
4
+ use { k8s_openapi:: api:: core:: v1:: PodSpec , std:: collections:: BTreeMap } ;
5
+
6
+ use indexmap:: IndexMap ;
3
7
use k8s_openapi:: api:: core:: v1:: {
4
8
ConfigMapKeySelector , Container , ContainerPort , EnvVar , EnvVarSource , Lifecycle ,
5
9
LifecycleHandler , ObjectFieldSelector , Probe , ResourceRequirements , SecretKeySelector ,
6
10
SecurityContext , VolumeMount ,
7
11
} ;
8
12
use snafu:: { ResultExt as _, Snafu } ;
13
+ use tracing:: instrument;
9
14
10
15
use crate :: {
11
16
commons:: product_image_selection:: ResolvedProductImage ,
@@ -21,11 +26,26 @@ pub enum Error {
21
26
source : validation:: Errors ,
22
27
container_name : String ,
23
28
} ,
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
+ } ,
24
39
}
25
40
26
41
/// A builder to build [`Container`] objects.
27
42
///
28
43
/// 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.
29
49
#[ derive( Clone , Default ) ]
30
50
pub struct ContainerBuilder {
31
51
args : Option < Vec < String > > ,
@@ -36,7 +56,9 @@ pub struct ContainerBuilder {
36
56
image_pull_policy : Option < String > ,
37
57
name : String ,
38
58
resources : Option < ResourceRequirements > ,
39
- volume_mounts : Option < Vec < VolumeMount > > ,
59
+
60
+ /// The key is the volumeMount mountPath.
61
+ volume_mounts : IndexMap < String , VolumeMount > ,
40
62
readiness_probe : Option < Probe > ,
41
63
liveness_probe : Option < Probe > ,
42
64
startup_probe : Option < Probe > ,
@@ -188,29 +210,79 @@ impl ContainerBuilder {
188
210
self
189
211
}
190
212
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.
191
261
pub fn add_volume_mount (
192
262
& mut self ,
193
263
name : impl Into < String > ,
194
264
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
+ } )
204
271
}
205
272
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.
206
277
pub fn add_volume_mounts (
207
278
& mut self ,
208
279
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 )
214
286
}
215
287
216
288
pub fn readiness_probe ( & mut self , probe : Probe ) -> & mut Self {
@@ -256,6 +328,12 @@ impl ContainerBuilder {
256
328
}
257
329
258
330
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
+
259
337
Container {
260
338
args : self . args . clone ( ) ,
261
339
command : self . command . clone ( ) ,
@@ -265,7 +343,7 @@ impl ContainerBuilder {
265
343
resources : self . resources . clone ( ) ,
266
344
name : self . name . clone ( ) ,
267
345
ports : self . container_ports . clone ( ) ,
268
- volume_mounts : self . volume_mounts . clone ( ) ,
346
+ volume_mounts,
269
347
readiness_probe : self . readiness_probe . clone ( ) ,
270
348
liveness_probe : self . liveness_probe . clone ( ) ,
271
349
startup_probe : self . startup_probe . clone ( ) ,
@@ -388,6 +466,7 @@ mod tests {
388
466
. add_env_var_from_config_map ( "envFromConfigMap" , "my-configmap" , "my-key" )
389
467
. add_env_var_from_secret ( "envFromSecret" , "my-secret" , "my-key" )
390
468
. add_volume_mount ( "configmap" , "/mount" )
469
+ . expect ( "add volume mount" )
391
470
. add_container_port ( container_port_name, container_port)
392
471
. resources ( resources. clone ( ) )
393
472
. add_container_ports ( vec ! [ ContainerPortBuilder :: new( container_port_1)
@@ -491,20 +570,18 @@ mod tests {
491
570
"lengthexceededlengthexceededlengthexceededlengthexceededlengthex" ;
492
571
assert_eq ! ( long_container_name. len( ) , 64 ) ; // 63 characters is the limit for container names
493
572
let result = ContainerBuilder :: new ( long_container_name) ;
494
- match result
573
+ if let Error :: InvalidContainerName {
574
+ container_name,
575
+ source,
576
+ } = result
495
577
. err ( )
496
578
. expect ( "Container name exceeding 63 characters should cause an error" )
497
579
{
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
+ )
508
585
}
509
586
// One characters shorter name is valid
510
587
let max_len_container_name: String = long_container_name. chars ( ) . skip ( 1 ) . collect ( ) ;
@@ -568,16 +645,14 @@ mod tests {
568
645
result : Result < ContainerBuilder , Error > ,
569
646
expected_err_contains : & str ,
570
647
) {
571
- match result
648
+ if let Error :: InvalidContainerName {
649
+ container_name : _,
650
+ source,
651
+ } = result
572
652
. err ( )
573
653
. expect ( "Container name exceeding 63 characters should cause an error" )
574
654
{
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) ) ) ;
581
656
}
582
657
}
583
658
}
0 commit comments