Skip to content

Commit c0c9c0d

Browse files
committed
WIP
1 parent a7e70f1 commit c0c9c0d

File tree

6 files changed

+116
-56
lines changed

6 files changed

+116
-56
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/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: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fmt;
22

3+
use indexmap::IndexMap;
34
use k8s_openapi::api::core::v1::{
45
ConfigMapKeySelector, Container, ContainerPort, EnvVar, EnvVarSource, Lifecycle,
56
LifecycleHandler, ObjectFieldSelector, Probe, ResourceRequirements, SecretKeySelector,
@@ -26,6 +27,11 @@ pub enum Error {
2627
/// A builder to build [`Container`] objects.
2728
///
2829
/// This will automatically create the necessary volumes and mounts for each `ConfigMap` which is added.
30+
///
31+
/// This struct is often times using an [`IndexMap`] to have consistent ordering (so we don't produce reconcile loops).
32+
/// We are also choosing it over an [`std::collections::BTreeMap`], as it's easier to debug for users, as logically
33+
/// grouped volumeMounts (e.g. all volumeMounts related to S3) are near each other in the list instead of "just" being
34+
/// sorted alphabetically.
2935
#[derive(Clone, Default)]
3036
pub struct ContainerBuilder {
3137
args: Option<Vec<String>>,
@@ -36,7 +42,9 @@ pub struct ContainerBuilder {
3642
image_pull_policy: Option<String>,
3743
name: String,
3844
resources: Option<ResourceRequirements>,
39-
volume_mounts: Option<Vec<VolumeMount>>,
45+
46+
/// The key is the volumeMount mountPath.
47+
volume_mounts: IndexMap<String, VolumeMount>,
4048
readiness_probe: Option<Probe>,
4149
liveness_probe: Option<Probe>,
4250
startup_probe: Option<Probe>,
@@ -188,28 +196,55 @@ impl ContainerBuilder {
188196
self
189197
}
190198

199+
/// This function only adds the [`VolumeMount`] in case there is no volumeMount with the same mountPath already.
200+
/// In case there already was a volumeMount with the same path already, an [`tracing::error`] is raised in case the
201+
/// contents of the volumeMounts differ.
202+
///
203+
/// Historically this function unconditionally added volumeMounts, which resulted in invalid
204+
/// [`k8s_openapi::api::core::v1::PodSpec`]s, as volumeMounts where added multiple times - think of Trino using the same [`crate::commons::s3::S3Connection`]
205+
/// two times, resulting in e.g. the s3 credentials being mounted twice as the same volumeMount.
206+
///
207+
/// We could have made this function fallible, but decided not to do so for now, as it would be a bigger breaking
208+
/// change.
209+
pub fn add_volume_mount_struct(&mut self, volume_mount: VolumeMount) -> &mut Self {
210+
if let Some(existing_volume_mount) = self.volume_mounts.get(&volume_mount.mount_path) {
211+
if !existing_volume_mount.eq(&volume_mount) {
212+
tracing::error!(
213+
?existing_volume_mount,
214+
new_volume_mount = ?volume_mount,
215+
"The operator tried to add a volumeMount to Container, which was already added with a different content! \
216+
The new volumeMount will be ignored."
217+
);
218+
}
219+
} else {
220+
self.volume_mounts
221+
.insert(volume_mount.mount_path.clone(), volume_mount);
222+
}
223+
224+
self
225+
}
226+
227+
/// See [`Self::add_volume_mount_struct`] for details
191228
pub fn add_volume_mount(
192229
&mut self,
193230
name: impl Into<String>,
194231
path: impl Into<String>,
195232
) -> &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
233+
self.add_volume_mount_struct(VolumeMount {
234+
name: name.into(),
235+
mount_path: path.into(),
236+
..VolumeMount::default()
237+
})
204238
}
205239

240+
/// See [`Self::add_volume_mount_struct`] for details
206241
pub fn add_volume_mounts(
207242
&mut self,
208243
volume_mounts: impl IntoIterator<Item = VolumeMount>,
209244
) -> &mut Self {
210-
self.volume_mounts
211-
.get_or_insert_with(Vec::new)
212-
.extend(volume_mounts);
245+
for volume_mount in volume_mounts {
246+
self.add_volume_mount_struct(volume_mount);
247+
}
213248
self
214249
}
215250

@@ -265,7 +300,11 @@ impl ContainerBuilder {
265300
resources: self.resources.clone(),
266301
name: self.name.clone(),
267302
ports: self.container_ports.clone(),
268-
volume_mounts: self.volume_mounts.clone(),
303+
volume_mounts: if self.volume_mounts.is_empty() {
304+
None
305+
} else {
306+
Some(self.volume_mounts.clone().into_values().collect())
307+
},
269308
readiness_probe: self.readiness_probe.clone(),
270309
liveness_probe: self.liveness_probe.clone(),
271310
startup_probe: self.startup_probe.clone(),

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

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{collections::BTreeMap, num::TryFromIntError};
22

3+
use indexmap::IndexMap;
34
use k8s_openapi::{
45
api::core::v1::{
56
Affinity, Container, LocalObjectReference, NodeAffinity, Pod, PodAffinity, PodAntiAffinity,
@@ -53,6 +54,10 @@ pub enum Error {
5354
}
5455

5556
/// A builder to build [`Pod`] or [`PodTemplateSpec`] objects.
57+
///
58+
/// This struct is often times using an [`IndexMap`] to have consistent ordering (so we don't produce reconcile loops).
59+
/// We are also choosing it over an [`BTreeMap`], as it's easier to debug for users, as logically grouped volumes
60+
/// (e.g. all volumes related to S3) are near each other in the list instead of "just" being sorted alphabetically.
5661
#[derive(Clone, Debug, Default, PartialEq)]
5762
pub struct PodBuilder {
5863
containers: Vec<Container>,
@@ -67,7 +72,9 @@ pub struct PodBuilder {
6772
status: Option<PodStatus>,
6873
security_context: Option<PodSecurityContext>,
6974
tolerations: Option<Vec<Toleration>>,
70-
volumes: Option<Vec<Volume>>,
75+
76+
/// The key is the volume name.
77+
volumes: IndexMap<String, Volume>,
7178
service_account_name: Option<String>,
7279
image_pull_secrets: Option<Vec<LocalObjectReference>>,
7380
restart_policy: Option<String>,
@@ -254,11 +261,6 @@ impl PodBuilder {
254261
self
255262
}
256263

257-
pub fn add_volume(&mut self, volume: Volume) -> &mut Self {
258-
self.volumes.get_or_insert_with(Vec::new).push(volume);
259-
self
260-
}
261-
262264
/// Utility function for the common case of adding an emptyDir Volume
263265
/// with the given name and no medium and no quantity.
264266
pub fn add_empty_dir_volume(
@@ -273,8 +275,39 @@ impl PodBuilder {
273275
)
274276
}
275277

278+
/// This function only adds the [`Volume`] in case there is no volume with the same name already.
279+
/// In case there already was a volume with the same name already, an [`tracing::error`] is raised in case the
280+
/// contents of the volumes differ.
281+
///
282+
/// Historically this function unconditionally added volumes, which resulted in invalid [`PodSpec`]s, as volumes
283+
/// where added multiple times - think of Trino using the same [`crate::commons::s3::S3Connection`] two times,
284+
/// resulting in e.g. the s3 credentials being mounted twice as the sake volume.
285+
///
286+
/// We could have made this function fallible, but decided not to do so for now, as it would be a bigger breaking
287+
/// change.
288+
pub fn add_volume(&mut self, volume: Volume) -> &mut Self {
289+
if let Some(existing_volume) = self.volumes.get(&volume.name) {
290+
if !existing_volume.eq(&volume) {
291+
tracing::error!(
292+
?existing_volume,
293+
new_volume = ?volume,
294+
"The operator tried to add a volume to Pod, which was already added with a different content! \
295+
The new volume will be ignored."
296+
);
297+
}
298+
} else {
299+
self.volumes.insert(volume.name.clone(), volume);
300+
}
301+
302+
self
303+
}
304+
305+
/// See [`Self::add_volume`] for details
276306
pub fn add_volumes(&mut self, volumes: Vec<Volume>) -> &mut Self {
277-
self.volumes.get_or_insert_with(Vec::new).extend(volumes);
307+
for volume in volumes {
308+
self.add_volume(volume);
309+
}
310+
278311
self
279312
}
280313

@@ -533,7 +566,11 @@ impl PodBuilder {
533566
}),
534567
security_context: self.security_context.clone(),
535568
tolerations: self.tolerations.clone(),
536-
volumes: self.volumes.clone(),
569+
volumes: if self.volumes.is_empty() {
570+
None
571+
} else {
572+
Some(self.volumes.clone().into_values().collect())
573+
},
537574
// Legacy feature for ancient Docker images
538575
// In practice, this just causes a bunch of unused environment variables that may conflict with other uses,
539576
// such as https://github.com/stackabletech/spark-operator/pull/256.

crates/stackable-operator/src/commons/s3/helpers.rs

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,12 @@ impl ResolvedS3Connection {
7979
///
8080
/// * Credentials needed to connect to S3
8181
/// * Needed TLS volumes
82-
///
83-
/// `unique_identifier` needs to be a unique identifier (e.g. in case of trino-operator the name of the catalog),
84-
/// so that multiple mounts of the same SecretClass do not produce clashing volumes and volumeMounts.
8582
pub fn add_volumes_and_mounts(
8683
&self,
87-
unique_identifier: &str,
8884
pod_builder: &mut PodBuilder,
8985
container_builders: Vec<&mut ContainerBuilder>,
9086
) -> Result<(), S3Error> {
91-
let (volumes, mounts) = self.volumes_and_mounts(unique_identifier)?;
87+
let (volumes, mounts) = self.volumes_and_mounts()?;
9288
pod_builder.add_volumes(volumes);
9389
for cb in container_builders {
9490
cb.add_volume_mounts(mounts.clone());
@@ -99,28 +95,22 @@ impl ResolvedS3Connection {
9995

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

109102
if let Some(credentials) = &self.credentials {
110103
let secret_class = &credentials.secret_class;
111-
let volume_name = format!("{unique_identifier}-{secret_class}-s3-credentials");
104+
let volume_name = format!("{secret_class}-s3-credentials");
112105

113106
volumes.push(
114107
credentials
115108
.to_volume(&volume_name)
116109
.context(AddS3CredentialVolumesSnafu)?,
117110
);
118111
mounts.push(
119-
VolumeMountBuilder::new(
120-
volume_name,
121-
format!("{SECRET_BASE_PATH}/{unique_identifier}-{secret_class}"),
122-
)
123-
.build(),
112+
VolumeMountBuilder::new(volume_name, format!("{SECRET_BASE_PATH}/{secret_class}"))
113+
.build(),
124114
);
125115
}
126116

@@ -137,15 +127,12 @@ impl ResolvedS3Connection {
137127

138128
/// Returns the path of the files containing bind user and password.
139129
/// This will be None if there are no credentials for this LDAP connection.
140-
///
141-
/// `unique_identifier` needs to be a unique identifier (e.g. in case of trino-operator the name of the catalog),
142-
/// so that multiple mounts of the same SecretClass do not produce clashing volumes and volumeMounts.
143-
pub fn credentials_mount_paths(&self, unique_identifier: &str) -> Option<(String, String)> {
130+
pub fn credentials_mount_paths(&self) -> Option<(String, String)> {
144131
self.credentials.as_ref().map(|bind_credentials| {
145132
let secret_class = &bind_credentials.secret_class;
146133
(
147-
format!("{SECRET_BASE_PATH}/{unique_identifier}-{secret_class}/accessKey"),
148-
format!("{SECRET_BASE_PATH}/{unique_identifier}-{secret_class}/secretKey"),
134+
format!("{SECRET_BASE_PATH}/{secret_class}/accessKey"),
135+
format!("{SECRET_BASE_PATH}/{secret_class}/secretKey"),
149136
)
150137
})
151138
}
@@ -214,7 +201,7 @@ mod test {
214201
credentials: None,
215202
tls: TlsClientDetails { tls: None },
216203
};
217-
let (volumes, mounts) = s3.volumes_and_mounts("lakehouse").unwrap();
204+
let (volumes, mounts) = s3.volumes_and_mounts().unwrap();
218205

219206
assert_eq!(s3.endpoint().unwrap(), Url::parse("http://minio").unwrap());
220207
assert_eq!(volumes, vec![]);
@@ -239,7 +226,7 @@ mod test {
239226
}),
240227
},
241228
};
242-
let (mut volumes, mut mounts) = s3.volumes_and_mounts("lakehouse").unwrap();
229+
let (mut volumes, mut mounts) = s3.volumes_and_mounts().unwrap();
243230

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

253-
assert_eq!(
254-
&volume.name,
255-
"lakehouse-ionos-s3-credentials-s3-credentials"
256-
);
240+
assert_eq!(&volume.name, "ionos-s3-credentials-s3-credentials");
257241
assert_eq!(
258242
&volume
259243
.ephemeral
@@ -271,15 +255,12 @@ mod test {
271255
);
272256

273257
assert_eq!(mount.name, volume.name);
258+
assert_eq!(mount.mount_path, "/stackable/secrets/ionos-s3-credentials");
274259
assert_eq!(
275-
mount.mount_path,
276-
"/stackable/secrets/lakehouse-ionos-s3-credentials"
277-
);
278-
assert_eq!(
279-
s3.credentials_mount_paths("lakehouse"),
260+
s3.credentials_mount_paths(),
280261
Some((
281-
"/stackable/secrets/lakehouse-ionos-s3-credentials/accessKey".to_string(),
282-
"/stackable/secrets/lakehouse-ionos-s3-credentials/secretKey".to_string()
262+
"/stackable/secrets/ionos-s3-credentials/accessKey".to_string(),
263+
"/stackable/secrets/ionos-s3-credentials/secretKey".to_string()
283264
))
284265
);
285266
}
@@ -297,7 +278,7 @@ mod test {
297278
}),
298279
},
299280
};
300-
let (volumes, mounts) = s3.volumes_and_mounts("lakehouse").unwrap();
281+
let (volumes, mounts) = s3.volumes_and_mounts().unwrap();
301282

302283
assert_eq!(
303284
s3.endpoint().unwrap(),

0 commit comments

Comments
 (0)