Skip to content

Commit 113c4ee

Browse files
maltesandersbernauerTechassi
authored
Fix: Invalid app version label when using hash in custom image (#1076)
* fix invalid app version label when using hash * adapted changelog * Apply suggestions from code review Co-authored-by: Sebastian Bernauer <[email protected]> * rename error * Update crates/stackable-operator/CHANGELOG.md Co-authored-by: Techassi <[email protected]> * rename app_version_label to app_version_label_value * fix doc linter * fix: Bump slab to 0.4.11 to fix RUSTSEC-2025-0047 Also see https://rustsec.org/advisories/RUSTSEC-2025-0047 * move field name change under changed section * linter --------- Co-authored-by: Sebastian Bernauer <[email protected]> Co-authored-by: Techassi <[email protected]>
1 parent 958f62f commit 113c4ee

File tree

7 files changed

+86
-39
lines changed

7 files changed

+86
-39
lines changed

Cargo.lock

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

crates/stackable-operator/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
9+
- BREAKING: The `ResolvedProductImage` field `app_version_label` was renamed to `app_version_label_value` to match changes to its type ([#1076]).
10+
11+
### Fixed
12+
13+
- BREAKING: Fix bug where `ResolvedProductImage::app_version_label` could not be used as a label value because it can contain invalid characters.
14+
This is the case when referencing custom images via a `@sha256:...` hash. As such, the `product_image_selection::resolve` function is now fallible ([#1076]).
15+
16+
[#1076]: https://github.com/stackabletech/operator-rs/pull/1076
17+
718
## [0.94.0] - 2025-07-10
819

920
### Added

crates/stackable-operator/src/commons/product_image_selection.rs

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ use dockerfile_parser::ImageRef;
22
use k8s_openapi::api::core::v1::LocalObjectReference;
33
use schemars::JsonSchema;
44
use serde::{Deserialize, Serialize};
5+
use snafu::{ResultExt, Snafu};
56
use strum::AsRefStr;
67

7-
#[cfg(doc)]
8-
use crate::kvp::Labels;
8+
use crate::kvp::{LABEL_VALUE_MAX_LEN, LabelValue, LabelValueError};
99

1010
pub const STACKABLE_DOCKER_REPO: &str = "oci.stackable.tech/sdp";
1111

12+
#[derive(Debug, Snafu)]
13+
pub enum Error {
14+
#[snafu(display("could not parse or create label from app version {app_version:?}"))]
15+
ParseAppVersionLabel {
16+
source: LabelValueError,
17+
app_version: String,
18+
},
19+
}
20+
1221
/// Specify which image to use, the easiest way is to only configure the `productVersion`.
1322
/// You can also configure a custom image registry to pull from, as well as completely custom
1423
/// images.
@@ -67,8 +76,8 @@ pub struct ResolvedProductImage {
6776
/// Version of the product, e.g. `1.4.1`.
6877
pub product_version: String,
6978

70-
/// App version as formatted for [`Labels::recommended`]
71-
pub app_version_label: String,
79+
/// App version formatted for Labels
80+
pub app_version_label_value: LabelValue,
7281

7382
/// Image to be used for the product image e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0`
7483
pub image: String,
@@ -101,7 +110,11 @@ impl ProductImage {
101110
/// `image_base_name` should be base of the image name in the container image registry, e.g. `trino`.
102111
/// `operator_version` needs to be the full operator version and a valid semver string.
103112
/// Accepted values are `23.7.0`, `0.0.0-dev` or `0.0.0-pr123`. Other variants are not supported.
104-
pub fn resolve(&self, image_base_name: &str, operator_version: &str) -> ResolvedProductImage {
113+
pub fn resolve(
114+
&self,
115+
image_base_name: &str,
116+
operator_version: &str,
117+
) -> Result<ResolvedProductImage, Error> {
105118
let image_pull_policy = self.pull_policy.as_ref().to_string();
106119
let pull_secrets = self.pull_secrets.clone();
107120

@@ -111,17 +124,17 @@ impl ProductImage {
111124
ProductImageSelection::Custom(image_selection) => {
112125
let image = ImageRef::parse(&image_selection.custom);
113126
let image_tag_or_hash = image.tag.or(image.hash).unwrap_or("latest".to_string());
114-
let mut app_version_label = format!("{}-{}", product_version, image_tag_or_hash);
115-
// TODO Use new label mechanism once added
116-
app_version_label.truncate(63);
117127

118-
ResolvedProductImage {
128+
let app_version = format!("{}-{}", product_version, image_tag_or_hash);
129+
let app_version_label_value = Self::prepare_app_version_label_value(&app_version)?;
130+
131+
Ok(ResolvedProductImage {
119132
product_version,
120-
app_version_label,
133+
app_version_label_value,
121134
image: image_selection.custom.clone(),
122135
image_pull_policy,
123136
pull_secrets,
124-
}
137+
})
125138
}
126139
ProductImageSelection::StackableVersion(image_selection) => {
127140
let repo = image_selection
@@ -147,14 +160,15 @@ impl ProductImage {
147160
let image = format!(
148161
"{repo}/{image_base_name}:{product_version}-stackable{stackable_version}",
149162
);
150-
let app_version_label = format!("{product_version}-stackable{stackable_version}",);
151-
ResolvedProductImage {
163+
let app_version = format!("{product_version}-stackable{stackable_version}");
164+
let app_version_label_value = Self::prepare_app_version_label_value(&app_version)?;
165+
Ok(ResolvedProductImage {
152166
product_version,
153-
app_version_label,
167+
app_version_label_value,
154168
image,
155169
image_pull_policy,
156170
pull_secrets,
157-
}
171+
})
158172
}
159173
}
160174
}
@@ -174,6 +188,21 @@ impl ProductImage {
174188
}) => pv,
175189
}
176190
}
191+
192+
fn prepare_app_version_label_value(app_version: &str) -> Result<LabelValue, Error> {
193+
let mut formatted_app_version = app_version.to_string();
194+
// Labels cannot have more than `LABEL_VALUE_MAX_LEN` characters.
195+
formatted_app_version.truncate(LABEL_VALUE_MAX_LEN);
196+
// The hash has the format `sha256:85fa483aa99b9997ce476b86893ad5ed81fb7fd2db602977eb`
197+
// As the colon (`:`) is not a valid label value character, we replace it with a valid "-" character.
198+
let formatted_app_version = formatted_app_version.replace(":", "-");
199+
200+
formatted_app_version
201+
.parse()
202+
.with_context(|_| ParseAppVersionLabelSnafu {
203+
app_version: formatted_app_version.to_string(),
204+
})
205+
}
177206
}
178207

179208
#[cfg(test)]
@@ -191,7 +220,7 @@ mod tests {
191220
"#,
192221
ResolvedProductImage {
193222
image: "oci.stackable.tech/sdp/superset:1.4.1-stackable23.7.42".to_string(),
194-
app_version_label: "1.4.1-stackable23.7.42".to_string(),
223+
app_version_label_value: "1.4.1-stackable23.7.42".parse().expect("static app version label is always valid"),
195224
product_version: "1.4.1".to_string(),
196225
image_pull_policy: "Always".to_string(),
197226
pull_secrets: None,
@@ -205,7 +234,7 @@ mod tests {
205234
"#,
206235
ResolvedProductImage {
207236
image: "oci.stackable.tech/sdp/superset:1.4.1-stackable0.0.0-dev".to_string(),
208-
app_version_label: "1.4.1-stackable0.0.0-dev".to_string(),
237+
app_version_label_value: "1.4.1-stackable0.0.0-dev".parse().expect("static app version label is always valid"),
209238
product_version: "1.4.1".to_string(),
210239
image_pull_policy: "Always".to_string(),
211240
pull_secrets: None,
@@ -219,7 +248,7 @@ mod tests {
219248
"#,
220249
ResolvedProductImage {
221250
image: "oci.stackable.tech/sdp/superset:1.4.1-stackable0.0.0-dev".to_string(),
222-
app_version_label: "1.4.1-stackable0.0.0-dev".to_string(),
251+
app_version_label_value: "1.4.1-stackable0.0.0-dev".parse().expect("static app version label is always valid"),
223252
product_version: "1.4.1".to_string(),
224253
image_pull_policy: "Always".to_string(),
225254
pull_secrets: None,
@@ -234,7 +263,7 @@ mod tests {
234263
"#,
235264
ResolvedProductImage {
236265
image: "oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0".to_string(),
237-
app_version_label: "1.4.1-stackable2.1.0".to_string(),
266+
app_version_label_value: "1.4.1-stackable2.1.0".parse().expect("static app version label is always valid"),
238267
product_version: "1.4.1".to_string(),
239268
image_pull_policy: "Always".to_string(),
240269
pull_secrets: None,
@@ -250,7 +279,7 @@ mod tests {
250279
"#,
251280
ResolvedProductImage {
252281
image: "my.corp/myteam/stackable/trino:1.4.1-stackable2.1.0".to_string(),
253-
app_version_label: "1.4.1-stackable2.1.0".to_string(),
282+
app_version_label_value: "1.4.1-stackable2.1.0".parse().expect("static app version label is always valid"),
254283
product_version: "1.4.1".to_string(),
255284
image_pull_policy: "Always".to_string(),
256285
pull_secrets: None,
@@ -265,7 +294,7 @@ mod tests {
265294
"#,
266295
ResolvedProductImage {
267296
image: "my.corp/myteam/stackable/superset".to_string(),
268-
app_version_label: "1.4.1-latest".to_string(),
297+
app_version_label_value: "1.4.1-latest".parse().expect("static app version label is always valid"),
269298
product_version: "1.4.1".to_string(),
270299
image_pull_policy: "Always".to_string(),
271300
pull_secrets: None,
@@ -280,7 +309,7 @@ mod tests {
280309
"#,
281310
ResolvedProductImage {
282311
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
283-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
312+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
284313
product_version: "1.4.1".to_string(),
285314
image_pull_policy: "Always".to_string(),
286315
pull_secrets: None,
@@ -295,7 +324,7 @@ mod tests {
295324
"#,
296325
ResolvedProductImage {
297326
image: "127.0.0.1:8080/myteam/stackable/superset".to_string(),
298-
app_version_label: "1.4.1-latest".to_string(),
327+
app_version_label_value: "1.4.1-latest".parse().expect("static app version label is always valid"),
299328
product_version: "1.4.1".to_string(),
300329
image_pull_policy: "Always".to_string(),
301330
pull_secrets: None,
@@ -310,7 +339,7 @@ mod tests {
310339
"#,
311340
ResolvedProductImage {
312341
image: "127.0.0.1:8080/myteam/stackable/superset:latest-and-greatest".to_string(),
313-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
342+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
314343
product_version: "1.4.1".to_string(),
315344
image_pull_policy: "Always".to_string(),
316345
pull_secrets: None,
@@ -325,7 +354,7 @@ mod tests {
325354
"#,
326355
ResolvedProductImage {
327356
image: "oci.stackable.tech/sdp/superset@sha256:85fa483aa99b9997ce476b86893ad5ed81fb7fd2db602977eb8c42f76efc1098".to_string(),
328-
app_version_label: "1.4.1-sha256:85fa483aa99b9997ce476b86893ad5ed81fb7fd2db602977eb".to_string(),
357+
app_version_label_value: "1.4.1-sha256-85fa483aa99b9997ce476b86893ad5ed81fb7fd2db602977eb".parse().expect("static app version label is always valid"),
329358
product_version: "1.4.1".to_string(),
330359
image_pull_policy: "Always".to_string(),
331360
pull_secrets: None,
@@ -341,7 +370,7 @@ mod tests {
341370
"#,
342371
ResolvedProductImage {
343372
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
344-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
373+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
345374
product_version: "1.4.1".to_string(),
346375
image_pull_policy: "Always".to_string(),
347376
pull_secrets: None,
@@ -357,7 +386,7 @@ mod tests {
357386
"#,
358387
ResolvedProductImage {
359388
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
360-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
389+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
361390
product_version: "1.4.1".to_string(),
362391
image_pull_policy: "IfNotPresent".to_string(),
363392
pull_secrets: None,
@@ -373,7 +402,7 @@ mod tests {
373402
"#,
374403
ResolvedProductImage {
375404
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
376-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
405+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
377406
product_version: "1.4.1".to_string(),
378407
image_pull_policy: "Always".to_string(),
379408
pull_secrets: None,
@@ -389,7 +418,7 @@ mod tests {
389418
"#,
390419
ResolvedProductImage {
391420
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
392-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
421+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
393422
product_version: "1.4.1".to_string(),
394423
image_pull_policy: "Never".to_string(),
395424
pull_secrets: None,
@@ -408,7 +437,7 @@ mod tests {
408437
"#,
409438
ResolvedProductImage {
410439
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
411-
app_version_label: "1.4.1-latest-and-greatest".to_string(),
440+
app_version_label_value: "1.4.1-latest-and-greatest".parse().expect("static app version label is always valid"),
412441
product_version: "1.4.1".to_string(),
413442
image_pull_policy: "Always".to_string(),
414443
pull_secrets: Some(vec![LocalObjectReference{name: "myPullSecrets1".to_string()}, LocalObjectReference{name: "myPullSecrets2".to_string()}]),
@@ -421,7 +450,9 @@ mod tests {
421450
#[case] expected: ResolvedProductImage,
422451
) {
423452
let product_image: ProductImage = serde_yaml::from_str(&input).expect("Illegal test input");
424-
let resolved_product_image = product_image.resolve(&image_base_name, &operator_version);
453+
let resolved_product_image = product_image
454+
.resolve(&image_base_name, &operator_version)
455+
.expect("Illegal test input");
425456

426457
assert_eq!(resolved_product_image, expected);
427458
}

crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ mod tests {
374374

375375
let resolved_product_image = ResolvedProductImage {
376376
image: "oci.stackable.tech/sdp/product:latest".to_string(),
377-
app_version_label: "1.0.0-latest".to_string(),
377+
app_version_label_value: "1.0.0-latest"
378+
.parse()
379+
.expect("static app version label is always valid"),
378380
product_version: "1.0.0".to_string(),
379381
image_pull_policy: "Always".to_string(),
380382
pull_secrets: None,
@@ -439,7 +441,9 @@ mod tests {
439441

440442
let resolved_product_image = ResolvedProductImage {
441443
image: "oci.stackable.tech/sdp/product:latest".to_string(),
442-
app_version_label: "1.0.0-latest".to_string(),
444+
app_version_label_value: "1.0.0-latest"
445+
.parse()
446+
.expect("static app version label is always valid"),
443447
product_version: "1.0.0".to_string(),
444448
image_pull_policy: "Always".to_string(),
445449
pull_secrets: None,

crates/stackable-operator/src/kvp/label/value.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use std::{fmt::Display, ops::Deref, str::FromStr, sync::LazyLock};
22

33
use regex::Regex;
4+
use schemars::JsonSchema;
45
use snafu::{Snafu, ensure};
56

67
use crate::kvp::Value;
78

8-
const LABEL_VALUE_MAX_LEN: usize = 63;
9+
pub const LABEL_VALUE_MAX_LEN: usize = 63;
910

1011
// Lazily initialized regular expressions
1112
static LABEL_VALUE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
@@ -43,7 +44,7 @@ pub enum LabelValueError {
4344
/// unvalidated mutable access to inner values.
4445
///
4546
/// [k8s-labels]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
46-
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
47+
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, JsonSchema)]
4748
pub struct LabelValue(String);
4849

4950
impl Value for LabelValue {

crates/stackable-operator/src/kvp/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ pub struct ObjectLabels<'a, T> {
380380
///
381381
/// However, this is pure documentation and should not be parsed.
382382
///
383-
/// [avl]: crate::commons::product_image_selection::ResolvedProductImage::app_version_label
383+
/// [avl]: crate::commons::product_image_selection::ResolvedProductImage::app_version_label_value
384384
pub app_version: &'a str,
385385

386386
/// The DNS-style name of the operator managing the object (such as `zookeeper.stackable.tech`)

crates/stackable-operator/src/product_logging/framework.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,7 @@ sinks:
13821382
///
13831383
/// # let resolved_product_image = ResolvedProductImage {
13841384
/// # product_version: "1.0.0".into(),
1385-
/// # app_version_label: "1.0.0".into(),
1385+
/// # app_version_label_value: "1.0.0".parse().expect("static app version label is always valid"),
13861386
/// # image: "oci.stackable.tech/sdp/my-product:1.0.0-stackable1.0.0".into(),
13871387
/// # image_pull_policy: "Always".into(),
13881388
/// # pull_secrets: None,

0 commit comments

Comments
 (0)