Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: Avoid clashing volumes and mounts #871

Merged
merged 28 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c0c9c0d
WIP
sbernauer Sep 20, 2024
a508c90
changelog
sbernauer Sep 20, 2024
6288d21
changelog
sbernauer Sep 20, 2024
5b25c0d
Make fn fallible
sbernauer Sep 20, 2024
1882e2f
clippy
sbernauer Sep 20, 2024
9ce0c8d
fix: Put Volume behind a Box
sbernauer Sep 20, 2024
af6f830
cargo fmt
sbernauer Sep 23, 2024
f689dd3
Apply suggestions from code review
sbernauer Sep 23, 2024
a14eecf
fix docs
sbernauer Sep 23, 2024
ddfc266
Update crates/stackable-operator/src/builder/pod/container.rs
sbernauer Sep 23, 2024
78ca3af
Merge branch 'main' into fix/clashing-volumes-and-mounts-2
sbernauer Sep 23, 2024
53ef278
expect
sbernauer Sep 23, 2024
1a3f95f
Use ==
sbernauer Sep 23, 2024
6518daa
Apply suggestions from code review
sbernauer Sep 23, 2024
a814f41
Make add_volume_mount_impl private
sbernauer Sep 23, 2024
b7fcdaa
Move variables out
sbernauer Sep 23, 2024
78bd455
Merge branch 'main' into fix/clashing-volumes-and-mounts-2
sbernauer Sep 24, 2024
cdf5c36
Carry indivual fields in error message
sbernauer Sep 25, 2024
8727f9a
Reduce Error enum size by boxing ClashingMountPathDetails
sbernauer Sep 25, 2024
e8220b9
Merge branch 'main' into fix/clashing-volumes-and-mounts-2
sbernauer Sep 25, 2024
b7b43b8
Revert "Reduce Error enum size by boxing ClashingMountPathDetails"
sbernauer Sep 26, 2024
e40749a
remove subPathExpr
sbernauer Sep 26, 2024
5a4dd0e
doc comment
sbernauer Sep 26, 2024
8e773ee
Trace error details instead of including them in the error message
sbernauer Sep 26, 2024
cc6f5ae
error handling
sbernauer Sep 26, 2024
58c7092
docs :P
sbernauer Sep 26, 2024
246f0c8
Revert "expect"
sbernauer Sep 26, 2024
9886155
clashing -> colliding
sbernauer Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ecdsa = { version = "0.16.9", features = ["digest", "pem"] }
either = "1.13.0"
futures = "0.3.30"
futures-util = "0.3.30"
indexmap = "2.5"
hyper = { version = "1.4.1", features = ["full"] }
hyper-util = "0.1.8"
itertools = "0.13.0"
Expand Down
10 changes: 10 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Fixed

- Avoid clashing volumes and mounts by only adding volumes or mounts if they do not already exist ([#871]).

### Changed

- 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]).

[#871]: https://github.com/stackabletech/operator-rs/pull/871

## [0.76.0] - 2024-09-19

### Added
Expand Down
1 change: 1 addition & 0 deletions crates/stackable-operator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ derivative.workspace = true
dockerfile-parser.workspace = true
either.workspace = true
futures.workspace = true
indexmap.workspace = true
json-patch.workspace = true
k8s-openapi.workspace = true
kube.workspace = true
Expand Down
65 changes: 52 additions & 13 deletions crates/stackable-operator/src/builder/pod/container.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt;

use indexmap::IndexMap;
use k8s_openapi::api::core::v1::{
ConfigMapKeySelector, Container, ContainerPort, EnvVar, EnvVarSource, Lifecycle,
LifecycleHandler, ObjectFieldSelector, Probe, ResourceRequirements, SecretKeySelector,
Expand All @@ -26,6 +27,11 @@ pub enum Error {
/// A builder to build [`Container`] objects.
///
/// This will automatically create the necessary volumes and mounts for each `ConfigMap` which is added.
///
/// This struct is often times using an [`IndexMap`] to have consistent ordering (so we don't produce reconcile loops).
/// We are also choosing it over an [`std::collections::BTreeMap`], as it's easier to debug for users, as logically
/// grouped volumeMounts (e.g. all volumeMounts related to S3) are near each other in the list instead of "just" being
razvan marked this conversation as resolved.
Show resolved Hide resolved
/// sorted alphabetically.
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Clone, Default)]
pub struct ContainerBuilder {
args: Option<Vec<String>>,
Expand All @@ -36,7 +42,9 @@ pub struct ContainerBuilder {
image_pull_policy: Option<String>,
name: String,
resources: Option<ResourceRequirements>,
volume_mounts: Option<Vec<VolumeMount>>,

/// The key is the volumeMount mountPath.
volume_mounts: IndexMap<String, VolumeMount>,
readiness_probe: Option<Probe>,
liveness_probe: Option<Probe>,
startup_probe: Option<Probe>,
Expand Down Expand Up @@ -188,28 +196,55 @@ impl ContainerBuilder {
self
}

/// This function only adds the [`VolumeMount`] in case there is no volumeMount with the same mountPath already.
/// In case there already was a volumeMount with the same path already, an [`tracing::error`] is raised in case the
/// contents of the volumeMounts differ.
///
/// Historically this function unconditionally added volumeMounts, which resulted in invalid
/// [`k8s_openapi::api::core::v1::PodSpec`]s, as volumeMounts where added multiple times - think of Trino using the same [`crate::commons::s3::S3Connection`]
/// two times, resulting in e.g. the s3 credentials being mounted twice as the same volumeMount.
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
///
/// We could have made this function fallible, but decided not to do so for now, as it would be a bigger breaking
NickLarsenNZ marked this conversation as resolved.
Show resolved Hide resolved
/// change.
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
pub fn add_volume_mount_struct(&mut self, volume_mount: VolumeMount) -> &mut Self {
if let Some(existing_volume_mount) = self.volume_mounts.get(&volume_mount.mount_path) {
if !existing_volume_mount.eq(&volume_mount) {
tracing::error!(
?existing_volume_mount,
new_volume_mount = ?volume_mount,
"The operator tried to add a volumeMount to Container, which was already added with a different content! \
The new volumeMount will be ignored."
);
}
} else {
self.volume_mounts
.insert(volume_mount.mount_path.clone(), volume_mount);
}

self
}

/// See [`Self::add_volume_mount_struct`] for details
pub fn add_volume_mount(
&mut self,
name: impl Into<String>,
path: impl Into<String>,
) -> &mut Self {
self.volume_mounts
.get_or_insert_with(Vec::new)
.push(VolumeMount {
name: name.into(),
mount_path: path.into(),
..VolumeMount::default()
});
self
self.add_volume_mount_struct(VolumeMount {
name: name.into(),
mount_path: path.into(),
..VolumeMount::default()
})
}

/// See [`Self::add_volume_mount_struct`] for details
pub fn add_volume_mounts(
&mut self,
volume_mounts: impl IntoIterator<Item = VolumeMount>,
) -> &mut Self {
self.volume_mounts
.get_or_insert_with(Vec::new)
.extend(volume_mounts);
for volume_mount in volume_mounts {
self.add_volume_mount_struct(volume_mount);
}
self
}

Expand Down Expand Up @@ -265,7 +300,11 @@ impl ContainerBuilder {
resources: self.resources.clone(),
name: self.name.clone(),
ports: self.container_ports.clone(),
volume_mounts: self.volume_mounts.clone(),
volume_mounts: if self.volume_mounts.is_empty() {
None
} else {
Some(self.volume_mounts.clone().into_values().collect())
},
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
readiness_probe: self.readiness_probe.clone(),
liveness_probe: self.liveness_probe.clone(),
startup_probe: self.startup_probe.clone(),
Expand Down
53 changes: 45 additions & 8 deletions crates/stackable-operator/src/builder/pod/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{collections::BTreeMap, num::TryFromIntError};

use indexmap::IndexMap;
use k8s_openapi::{
api::core::v1::{
Affinity, Container, LocalObjectReference, NodeAffinity, Pod, PodAffinity, PodAntiAffinity,
Expand Down Expand Up @@ -53,6 +54,10 @@ pub enum Error {
}

/// A builder to build [`Pod`] or [`PodTemplateSpec`] objects.
///
/// This struct is often times using an [`IndexMap`] to have consistent ordering (so we don't produce reconcile loops).
/// We are also choosing it over an [`BTreeMap`], as it's easier to debug for users, as logically grouped volumes
/// (e.g. all volumes related to S3) are near each other in the list instead of "just" being sorted alphabetically.
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PodBuilder {
containers: Vec<Container>,
Expand All @@ -67,7 +72,9 @@ pub struct PodBuilder {
status: Option<PodStatus>,
security_context: Option<PodSecurityContext>,
tolerations: Option<Vec<Toleration>>,
volumes: Option<Vec<Volume>>,

/// The key is the volume name.
volumes: IndexMap<String, Volume>,
service_account_name: Option<String>,
image_pull_secrets: Option<Vec<LocalObjectReference>>,
restart_policy: Option<String>,
Expand Down Expand Up @@ -254,11 +261,6 @@ impl PodBuilder {
self
}

pub fn add_volume(&mut self, volume: Volume) -> &mut Self {
self.volumes.get_or_insert_with(Vec::new).push(volume);
self
}

/// Utility function for the common case of adding an emptyDir Volume
/// with the given name and no medium and no quantity.
pub fn add_empty_dir_volume(
Expand All @@ -273,8 +275,39 @@ impl PodBuilder {
)
}

/// This function only adds the [`Volume`] in case there is no volume with the same name already.
/// In case there already was a volume with the same name already, an [`tracing::error`] is raised in case the
/// contents of the volumes differ.
///
/// Historically this function unconditionally added volumes, which resulted in invalid [`PodSpec`]s, as volumes
/// where added multiple times - think of Trino using the same [`crate::commons::s3::S3Connection`] two times,
/// resulting in e.g. the s3 credentials being mounted twice as the sake volume.
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
///
/// We could have made this function fallible, but decided not to do so for now, as it would be a bigger breaking
/// change.
sbernauer marked this conversation as resolved.
Show resolved Hide resolved
pub fn add_volume(&mut self, volume: Volume) -> &mut Self {
if let Some(existing_volume) = self.volumes.get(&volume.name) {
if !existing_volume.eq(&volume) {
tracing::error!(
?existing_volume,
new_volume = ?volume,
"The operator tried to add a volume to Pod, which was already added with a different content! \
The new volume will be ignored."
);
}
} else {
self.volumes.insert(volume.name.clone(), volume);
}

self
}

/// See [`Self::add_volume`] for details
pub fn add_volumes(&mut self, volumes: Vec<Volume>) -> &mut Self {
self.volumes.get_or_insert_with(Vec::new).extend(volumes);
for volume in volumes {
self.add_volume(volume);
}

self
}

Expand Down Expand Up @@ -533,7 +566,11 @@ impl PodBuilder {
}),
security_context: self.security_context.clone(),
tolerations: self.tolerations.clone(),
volumes: self.volumes.clone(),
volumes: if self.volumes.is_empty() {
None
} else {
Some(self.volumes.clone().into_values().collect())
},
Techassi marked this conversation as resolved.
Show resolved Hide resolved
// Legacy feature for ancient Docker images
// In practice, this just causes a bunch of unused environment variables that may conflict with other uses,
// such as https://github.com/stackabletech/spark-operator/pull/256.
Expand Down
51 changes: 16 additions & 35 deletions crates/stackable-operator/src/commons/s3/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,12 @@ impl ResolvedS3Connection {
///
/// * Credentials needed to connect to S3
/// * Needed TLS volumes
///
/// `unique_identifier` needs to be a unique identifier (e.g. in case of trino-operator the name of the catalog),
/// so that multiple mounts of the same SecretClass do not produce clashing volumes and volumeMounts.
pub fn add_volumes_and_mounts(
&self,
unique_identifier: &str,
pod_builder: &mut PodBuilder,
container_builders: Vec<&mut ContainerBuilder>,
) -> Result<(), S3Error> {
let (volumes, mounts) = self.volumes_and_mounts(unique_identifier)?;
let (volumes, mounts) = self.volumes_and_mounts()?;
pod_builder.add_volumes(volumes);
for cb in container_builders {
cb.add_volume_mounts(mounts.clone());
Expand All @@ -99,28 +95,22 @@ impl ResolvedS3Connection {

/// It is recommended to use [`Self::add_volumes_and_mounts`], this function returns you the
/// volumes and mounts in case you need to add them by yourself.
pub fn volumes_and_mounts(
&self,
unique_identifier: &str,
) -> Result<(Vec<Volume>, Vec<VolumeMount>), S3Error> {
pub fn volumes_and_mounts(&self) -> Result<(Vec<Volume>, Vec<VolumeMount>), S3Error> {
let mut volumes = Vec::new();
let mut mounts = Vec::new();

if let Some(credentials) = &self.credentials {
let secret_class = &credentials.secret_class;
let volume_name = format!("{unique_identifier}-{secret_class}-s3-credentials");
let volume_name = format!("{secret_class}-s3-credentials");

volumes.push(
credentials
.to_volume(&volume_name)
.context(AddS3CredentialVolumesSnafu)?,
);
mounts.push(
VolumeMountBuilder::new(
volume_name,
format!("{SECRET_BASE_PATH}/{unique_identifier}-{secret_class}"),
)
.build(),
VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}"))
.build(),
);
}

Expand All @@ -137,15 +127,12 @@ impl ResolvedS3Connection {

/// Returns the path of the files containing bind user and password.
/// This will be None if there are no credentials for this LDAP connection.
///
/// `unique_identifier` needs to be a unique identifier (e.g. in case of trino-operator the name of the catalog),
/// so that multiple mounts of the same SecretClass do not produce clashing volumes and volumeMounts.
pub fn credentials_mount_paths(&self, unique_identifier: &str) -> Option<(String, String)> {
pub fn credentials_mount_paths(&self) -> Option<(String, String)> {
self.credentials.as_ref().map(|bind_credentials| {
let secret_class = &bind_credentials.secret_class;
(
format!("{SECRET_BASE_PATH}/{unique_identifier}-{secret_class}/accessKey"),
format!("{SECRET_BASE_PATH}/{unique_identifier}-{secret_class}/secretKey"),
format!("{SECRET_BASE_PATH}/{secret_class}/accessKey"),
format!("{SECRET_BASE_PATH}/{secret_class}/secretKey"),
)
})
}
Expand Down Expand Up @@ -214,7 +201,7 @@ mod test {
credentials: None,
tls: TlsClientDetails { tls: None },
};
let (volumes, mounts) = s3.volumes_and_mounts("lakehouse").unwrap();
let (volumes, mounts) = s3.volumes_and_mounts().unwrap();

assert_eq!(s3.endpoint().unwrap(), Url::parse("http://minio").unwrap());
assert_eq!(volumes, vec![]);
Expand All @@ -239,7 +226,7 @@ mod test {
}),
},
};
let (mut volumes, mut mounts) = s3.volumes_and_mounts("lakehouse").unwrap();
let (mut volumes, mut mounts) = s3.volumes_and_mounts().unwrap();

assert_eq!(
s3.endpoint().unwrap(),
Expand All @@ -250,10 +237,7 @@ mod test {
assert_eq!(mounts.len(), 1);
let mount = mounts.remove(0);

assert_eq!(
&volume.name,
"lakehouse-ionos-s3-credentials-s3-credentials"
);
assert_eq!(&volume.name, "ionos-s3-credentials-s3-credentials");
assert_eq!(
&volume
.ephemeral
Expand All @@ -271,15 +255,12 @@ mod test {
);

assert_eq!(mount.name, volume.name);
assert_eq!(mount.mount_path, "/stackable/secrets/ionos-s3-credentials");
assert_eq!(
mount.mount_path,
"/stackable/secrets/lakehouse-ionos-s3-credentials"
);
assert_eq!(
s3.credentials_mount_paths("lakehouse"),
s3.credentials_mount_paths(),
Some((
"/stackable/secrets/lakehouse-ionos-s3-credentials/accessKey".to_string(),
"/stackable/secrets/lakehouse-ionos-s3-credentials/secretKey".to_string()
"/stackable/secrets/ionos-s3-credentials/accessKey".to_string(),
"/stackable/secrets/ionos-s3-credentials/secretKey".to_string()
))
);
}
Expand All @@ -297,7 +278,7 @@ mod test {
}),
},
};
let (volumes, mounts) = s3.volumes_and_mounts("lakehouse").unwrap();
let (volumes, mounts) = s3.volumes_and_mounts().unwrap();

assert_eq!(
s3.endpoint().unwrap(),
Expand Down
Loading