Skip to content

Commit 95dcfd4

Browse files
sbernauerNickLarsenNZTechassi
authored
fix!: Avoid clashing volumes and mounts (#871)
* WIP * changelog * changelog * Make fn fallible * clippy * fix: Put Volume behind a Box * cargo fmt * Apply suggestions from code review Co-authored-by: Nick <[email protected]> * fix docs * Update crates/stackable-operator/src/builder/pod/container.rs Co-authored-by: Nick <[email protected]> * expect * Use == * Apply suggestions from code review Co-authored-by: Techassi <[email protected]> * Make add_volume_mount_impl private * Move variables out * Carry indivual fields in error message * Reduce Error enum size by boxing ClashingMountPathDetails * Revert "Reduce Error enum size by boxing ClashingMountPathDetails" This reverts commit 8727f9a. * remove subPathExpr * doc comment * Trace error details instead of including them in the error message * error handling * docs :P * Revert "expect" This reverts commit 53ef278. * clashing -> colliding --------- Co-authored-by: Nick <[email protected]> Co-authored-by: Techassi <[email protected]>
1 parent c3c6291 commit 95dcfd4

File tree

11 files changed

+274
-104
lines changed

11 files changed

+274
-104
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ecdsa = { version = "0.16.9", features = ["digest", "pem"] }
2525
either = "1.13.0"
2626
futures = "0.3.30"
2727
futures-util = "0.3.30"
28+
indexmap = "2.5"
2829
hyper = { version = "1.4.1", features = ["full"] }
2930
hyper-util = "0.1.8"
3031
itertools = "0.13.0"

crates/stackable-operator/CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ All notable changes to this project will be documented in this file.
66

77
### Fixed
88

9-
- Fix the logback configuration for logback versions from 1.3.6/1.4.6 to
10-
1.3.11/1.4.11 ([#874]).
9+
- Fix the logback configuration for logback versions from 1.3.6/1.4.6 to 1.3.11/1.4.11 ([#874]).
10+
- BREAKING: Avoid colliding volumes and mounts by only adding volumes or mounts if they do not already exist. This makes functions such as `PodBuilder::add_volume` or `ContainerBuilder::add_volume_mount` as well as related ones fallible ([#871]).
1111

12+
### Changed
13+
14+
- BREAKING: Remove the `unique_identifier` argument from `ResolvedS3Connection::add_volumes_and_mounts`, `ResolvedS3Connection::volumes_and_mounts` and `ResolvedS3Connection::credentials_mount_paths` as it is not needed anymore ([#871]).
15+
16+
[#871]: https://github.com/stackabletech/operator-rs/pull/871
1217
[#874]: https://github.com/stackabletech/operator-rs/pull/874
1318

1419
## [0.76.0] - 2024-09-19

crates/stackable-operator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ derivative.workspace = true
2121
dockerfile-parser.workspace = true
2222
either.workspace = true
2323
futures.workspace = true
24+
indexmap.workspace = true
2425
json-patch.workspace = true
2526
k8s-openapi.workspace = true
2627
kube.workspace = true

crates/stackable-operator/src/builder/pod/container.rs

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
use std::fmt;
22

3+
#[cfg(doc)]
4+
use {k8s_openapi::api::core::v1::PodSpec, std::collections::BTreeMap};
5+
6+
use indexmap::IndexMap;
37
use k8s_openapi::api::core::v1::{
48
ConfigMapKeySelector, Container, ContainerPort, EnvVar, EnvVarSource, Lifecycle,
59
LifecycleHandler, ObjectFieldSelector, Probe, ResourceRequirements, SecretKeySelector,
610
SecurityContext, VolumeMount,
711
};
812
use snafu::{ResultExt as _, Snafu};
13+
use tracing::instrument;
914

1015
use 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)]
3050
pub 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

Comments
 (0)