From 9c093080c7fa9ded58f9c706f4a43ff6051321f2 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 21 Apr 2026 13:49:59 +0200 Subject: [PATCH 1/2] feat(operator)!: Support dynamic product image selection --- .../stackable-operator/crds/DummyCluster.yaml | 27 ++- .../stackable-operator/src/cli/environment.rs | 21 +++ .../src/commons/product_image_selection.rs | 157 +++++++++++++----- 3 files changed, 162 insertions(+), 43 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index f6eb9e955..015530393 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -1781,8 +1781,9 @@ spec: properties: custom: description: |- - Overwrite the docker image. - Specify the full docker image name, e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` + Overwrite the container image. + + Specify the full container image name, e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` type: string productVersion: description: Version of the product, e.g. `1.4.1`. @@ -1808,15 +1809,29 @@ spec: type: object nullable: true type: array - repo: - description: Name of the docker repo, e.g. `oci.stackable.tech/sdp` + registry: + description: |- + The container image registry, e.g. `oci.stackable.tech`. + + If not specified, the operator will use the image registry provided via the operator + environment options. + nullable: true + type: string + repository: + description: |- + The repository on the container image registry where the container image is located, e.g. + `sdp/airflow`. + + If not specified, the operator will use the image registry provided via the operator + environment options. nullable: true type: string stackableVersion: description: |- Stackable version of the product, e.g. `23.4`, `23.4.1` or `0.0.0-dev`. - If not specified, the operator will use its own version, e.g. `23.4.1`. - When using a nightly operator or a pr version, it will use the nightly `0.0.0-dev` image. + + If not specified, the operator will use its own version, e.g. `23.4.1`. When using a nightly + operator or a PR version, it will use the nightly `0.0.0-dev` image. nullable: true type: string type: object diff --git a/crates/stackable-operator/src/cli/environment.rs b/crates/stackable-operator/src/cli/environment.rs index bf6d28f22..b62498d77 100644 --- a/crates/stackable-operator/src/cli/environment.rs +++ b/crates/stackable-operator/src/cli/environment.rs @@ -13,4 +13,25 @@ pub struct OperatorEnvironmentOptions { /// something like `-operator`. #[arg(long, env)] pub operator_service_name: String, + + /// The image registry which should be used when resolving images provisioned by the operator. + /// + /// Example values include: `127.0.0.1` or `oci.example.org`. + /// + /// Note that when running the operator on Kubernetes we recommend to provide this value via + /// the deployment mechanism, like Helm. + #[arg(long, env, value_parser = url::Host::parse)] + pub image_registry: url::Host, + + /// The image repository used in conjunction with the `image_registry` to form the final image + /// name. + /// + /// Example values include: `airflow-operator` or `path/to/hbase-operator`. + /// + /// Note that when running the operator on Kubernetes we recommend to provide this value via + /// the deployment mechanism, like Helm. Additionally, care must be taken when this value is + /// used as part of the product image selection, as it (most likely) includes the `-operator` + /// suffix. + #[arg(long, env)] + pub image_repository: String, } diff --git a/crates/stackable-operator/src/commons/product_image_selection.rs b/crates/stackable-operator/src/commons/product_image_selection.rs index a11d823df..6d52822dd 100644 --- a/crates/stackable-operator/src/commons/product_image_selection.rs +++ b/crates/stackable-operator/src/commons/product_image_selection.rs @@ -42,7 +42,7 @@ pub struct ProductImage { #[serde(rename_all = "camelCase")] #[serde(untagged)] pub enum ProductImageSelection { - // Order matters! + // NOTE: Order matters! // The variants will be tried from top to bottom Custom(ProductImageCustom), StackableVersion(ProductImageStackableVersion), @@ -51,9 +51,11 @@ pub enum ProductImageSelection { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProductImageCustom { - /// Overwrite the docker image. - /// Specify the full docker image name, e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` + /// Overwrite the container image. + /// + /// Specify the full container image name, e.g. `oci.stackable.tech/sdp/superset:1.4.1-stackable2.1.0` custom: String, + /// Version of the product, e.g. `1.4.1`. product_version: String, } @@ -63,12 +65,25 @@ pub struct ProductImageCustom { pub struct ProductImageStackableVersion { /// Version of the product, e.g. `1.4.1`. product_version: String, + /// Stackable version of the product, e.g. `23.4`, `23.4.1` or `0.0.0-dev`. - /// If not specified, the operator will use its own version, e.g. `23.4.1`. - /// When using a nightly operator or a pr version, it will use the nightly `0.0.0-dev` image. + /// + /// If not specified, the operator will use its own version, e.g. `23.4.1`. When using a nightly + /// operator or a PR version, it will use the nightly `0.0.0-dev` image. stackable_version: Option, - /// Name of the docker repo, e.g. `oci.stackable.tech/sdp` - repo: Option, + + /// The container image registry, e.g. `oci.stackable.tech`. + /// + /// If not specified, the operator will use the image registry provided via the operator + /// environment options. + registry: Option, + + /// The repository on the container image registry where the container image is located, e.g. + /// `sdp/airflow`. + /// + /// If not specified, the operator will use the image registry provided via the operator + /// environment options. + repository: Option, } #[derive(Clone, Debug, PartialEq, JsonSchema)] @@ -106,12 +121,23 @@ pub enum PullPolicy { } impl ProductImage { - /// `image_base_name` should be base of the image name in the container image registry, e.g. `trino`. - /// `operator_version` needs to be the full operator version and a valid semver string. - /// Accepted values are `23.7.0`, `0.0.0-dev` or `0.0.0-pr123`. Other variants are not supported. + /// Resolves the product image to be used for containers. + /// + /// ### Parameters + /// + /// - `image_registry`: The default image registry which should be used when no custom registry + /// is specified. This value should come from the operator environment options, which are + /// provided via Helm for example. Example value: `oci.example.org` + /// - `image_repository`: The default repository on the image registry where the container image + /// is located. This value should come from the operator environment options, which are + /// provided via Helm for example. Example value: `my/namespace/image`. + /// - `operator_version`: The version must be the full operator version and a valid semver + /// string. Accepted values are `23.7.0`, `0.0.0-dev` or `0.0.0-pr123`. Other variants are not + /// supported. pub fn resolve( &self, - image_base_name: &str, + image_registry: &str, + image_repository: &str, operator_version: &str, ) -> Result { let image_pull_policy = self.pull_policy.as_ref().to_string(); @@ -139,10 +165,16 @@ impl ProductImage { }) } ProductImageSelection::StackableVersion(image_selection) => { - let repo = image_selection - .repo + let registry = image_selection + .registry .as_deref() - .unwrap_or(STACKABLE_DOCKER_REPO); + .unwrap_or(image_registry); + + let repository = image_selection + .repository + .as_deref() + .unwrap_or(image_repository); + let stackable_version = match &image_selection.stackable_version { Some(stackable_version) => stackable_version, None => { @@ -159,11 +191,11 @@ impl ProductImage { } } }; - let image = format!( - "{repo}/{image_base_name}:{product_version}-stackable{stackable_version}", - ); + let app_version = format!("{product_version}-stackable{stackable_version}"); let app_version_label_value = Self::prepare_app_version_label_value(&app_version)?; + let image = format!("{registry}/{repository}:{app_version}",); + Ok(ResolvedProductImage { product_version, app_version_label_value, @@ -215,7 +247,8 @@ mod tests { #[rstest] #[case::stackable_version_without_stackable_version_stable_version( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" productVersion: 1.4.1 @@ -229,7 +262,8 @@ mod tests { } )] #[case::stackable_version_without_stackable_version_nightly( - "superset", + "oci.stackable.tech", + "sdp/superset", "0.0.0-dev", r" productVersion: 1.4.1 @@ -243,7 +277,8 @@ mod tests { } )] #[case::stackable_version_without_stackable_version_pr_version( - "superset", + "oci.stackable.tech", + "sdp/superset", "0.0.0-pr123", r" productVersion: 1.4.1 @@ -257,7 +292,8 @@ mod tests { } )] #[case::stackable_version_without_repo( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" productVersion: 1.4.1 @@ -271,16 +307,52 @@ mod tests { pull_secrets: None, } )] - #[case::stackable_version_with_repo( - "trino", + #[case::stackable_version_with_registry( + "oci.stackable.tech", + "sdp/trino", + "23.7.42", + r" + productVersion: 1.4.1 + stackableVersion: 2.1.0 + registry: oci.example.org + ", + ResolvedProductImage { + image: "oci.example.org/sdp/trino:1.4.1-stackable2.1.0".to_string(), + app_version_label_value: "1.4.1-stackable2.1.0".parse().expect("static app version label is always valid"), + product_version: "1.4.1".to_string(), + image_pull_policy: "Always".to_string(), + pull_secrets: None, + } + )] + #[case::stackable_version_with_repository( + "oci.stackable.tech", + "sdp/trino", + "23.7.42", + r" + productVersion: 1.4.1 + stackableVersion: 2.1.0 + repository: stackable/trino + ", + ResolvedProductImage { + image: "oci.stackable.tech/stackable/trino:1.4.1-stackable2.1.0".to_string(), + app_version_label_value: "1.4.1-stackable2.1.0".parse().expect("static app version label is always valid"), + product_version: "1.4.1".to_string(), + image_pull_policy: "Always".to_string(), + pull_secrets: None, + } + )] + #[case::stackable_version_with_registry_and_repository( + "oci.stackable.tech", + "sdp/trino", "23.7.42", r" productVersion: 1.4.1 stackableVersion: 2.1.0 - repo: my.corp/myteam/stackable + registry: quay.io + repository: stackable/trino ", ResolvedProductImage { - image: "my.corp/myteam/stackable/trino:1.4.1-stackable2.1.0".to_string(), + image: "quay.io/stackable/trino:1.4.1-stackable2.1.0".to_string(), app_version_label_value: "1.4.1-stackable2.1.0".parse().expect("static app version label is always valid"), product_version: "1.4.1".to_string(), image_pull_policy: "Always".to_string(), @@ -288,7 +360,8 @@ mod tests { } )] #[case::custom_without_tag( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset @@ -303,7 +376,8 @@ mod tests { } )] #[case::custom_with_tag( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset:latest-and-greatest @@ -318,7 +392,8 @@ mod tests { } )] #[case::custom_with_colon_in_repo_and_without_tag( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: 127.0.0.1:8080/myteam/stackable/superset @@ -333,7 +408,8 @@ mod tests { } )] #[case::custom_with_colon_in_repo_and_with_tag( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: 127.0.0.1:8080/myteam/stackable/superset:latest-and-greatest @@ -348,7 +424,8 @@ mod tests { } )] #[case::custom_with_hash_in_repo_and_without_tag( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: oci.stackable.tech/sdp/superset@sha256:85fa483aa99b9997ce476b86893ad5ed81fb7fd2db602977eb8c42f76efc1098 @@ -363,7 +440,8 @@ mod tests { } )] #[case::custom_takes_precedence( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset:latest-and-greatest @@ -379,7 +457,8 @@ mod tests { } )] #[case::pull_policy_if_not_present( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset:latest-and-greatest @@ -395,7 +474,8 @@ mod tests { } )] #[case::pull_policy_always( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset:latest-and-greatest @@ -411,7 +491,8 @@ mod tests { } )] #[case::pull_policy_never( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset:latest-and-greatest @@ -427,7 +508,8 @@ mod tests { } )] #[case::pull_secrets( - "superset", + "oci.stackable.tech", + "sdp/superset", "23.7.42", r" custom: my.corp/myteam/stackable/superset:latest-and-greatest @@ -446,14 +528,15 @@ mod tests { } )] fn resolved_image_pass( - #[case] image_base_name: String, + #[case] image_registry: String, + #[case] image_repository: String, #[case] operator_version: String, #[case] input: String, #[case] expected: ResolvedProductImage, ) { let product_image: ProductImage = serde_yaml::from_str(&input).expect("Illegal test input"); let resolved_product_image = product_image - .resolve(&image_base_name, &operator_version) + .resolve(&image_registry, &image_repository, &operator_version) .expect("Illegal test input"); assert_eq!(resolved_product_image, expected); From 3a13c9d60b87024bc86b6b2c6308e4d5a6c16062 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 21 Apr 2026 14:41:15 +0200 Subject: [PATCH 2/2] chore(operator): Add changelog entries --- crates/stackable-operator/CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 54eabd978..0fa39474e 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- BREAKING: Add two required CLI arguments and env vars to set image registry and repository ([#1199]): + - `IMAGE_REGISTRY` (`--image-registry`): Sets the image registry which should be used by the + operator to construct image names for provisioned containers, eg. `oci.example.org`. + - `IMAGE_REPOSITORY` (`--image-repository`): Sets the image repository which should be used by the + operator to construct image names for provisioned containers, eg. `my/repository/to/operator`. + +### Changed + +- BREAKING: The product image selection mechanism via `ProductImage::resolve` now takes three + parameters instead of two. The new parameters are: `image_registry`, `image_repository`, and + `operator_version`. +- BREAKING: The product image selection CRD interface splits up the `repo` key into `registry` and + `repository` for more clarity and consistency ([#1199]). + +[#1199]: https://github.com/stackabletech/operator-rs/pull/1199 + ## [0.110.1] - 2026-04-16 ### Added