Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7785ff5
Add common struct for product image selection
sbernauer Sep 26, 2022
7d66a49
Take by reference
sbernauer Sep 26, 2022
19c6ea6
WIP
sbernauer Sep 26, 2022
ce854e8
Default to auto
sbernauer Sep 26, 2022
1d70355
Add pull policy and secrets
sbernauer Sep 28, 2022
bb33389
Add some tests
sbernauer Sep 28, 2022
b2b14a7
Add to builders
sbernauer Oct 3, 2022
2859d55
Add product_version fn
sbernauer Oct 3, 2022
ac11f14
WIP
sbernauer Oct 3, 2022
0bda50f
Add JsonSchema
sbernauer Oct 3, 2022
1d43a58
Adopt tests
sbernauer Oct 6, 2022
4f43d9a
docs
sbernauer Oct 6, 2022
51897a7
cleanup
sbernauer Oct 6, 2022
f68fbbd
Merge remote-tracking branch 'origin/main' into feature/product-image…
sbernauer Oct 18, 2022
995f627
Remove kube native-tls feature
sbernauer Oct 18, 2022
768cd7a
Use kube 0.76.0
sbernauer Oct 28, 2022
160398d
Remove kube native-tls feature
sbernauer Oct 28, 2022
0fcbe8d
typo
sbernauer Oct 28, 2022
f538239
docs
sbernauer Oct 28, 2022
f6a38f0
docs
sbernauer Oct 28, 2022
b1d906b
docs
sbernauer Nov 4, 2022
6315fb9
Removed unused Default impl
sbernauer Nov 7, 2022
258ff6d
Merge branch 'feature/product-image-selection' of github.com:stackabl…
sbernauer Nov 7, 2022
1983187
Remove Stackable enum variant
sbernauer Nov 11, 2022
c882426
Merge remote-tracking branch 'origin/main' into feature/product-image…
sbernauer Nov 11, 2022
d44c2ad
changelog
sbernauer Nov 11, 2022
62b8031
Merge branch 'main' into feature/product-image-selection
sbernauer Nov 11, 2022
f3c9982
Add app_version_label to ResolvedProductImage
sbernauer Nov 11, 2022
74b57f4
Merge remote-tracking branch 'origin/main' into feature/product-image…
sbernauer Nov 11, 2022
b88d33f
Update src/commons/product_image_selection.rs
sbernauer Nov 11, 2022
a668a01
Update src/labels.rs
sbernauer Nov 11, 2022
55681ea
Update src/labels.rs
sbernauer Nov 11, 2022
e93edb0
Update src/labels.rs
sbernauer Nov 11, 2022
e267c1f
Update src/labels.rs
sbernauer Nov 11, 2022
372ba5c
Update src/labels.rs
sbernauer Nov 11, 2022
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Added product image selection struct ([#476]).

### Changed

- BREAKING: `get_recommended_labels` and `with_recommended_labels` now takes a struct of named arguments ([#501]).
- BREAKING: `get_recommended_labels` (and co) now takes the operator and controller names separately ([#492]).
- BREAKING: `ClusterResources` now takes the operator and controller names separately ([#492]).
- When upgrading, please use FQDN-style names for the operators (`{operator}.stackable.tech`).
- Bump kube to `0.76.0` ([#476]).
- Bump opentelemetry crates ([#502]).
- Bump clap to 4.0 ([#503]).

[#476]: https://github.com/stackabletech/operator-rs/pull/476
[#492]: https://github.com/stackabletech/operator-rs/pull/492
[#501]: https://github.com/stackabletech/operator-rs/pull/501
[#502]: https://github.com/stackabletech/operator-rs/pull/502
Expand Down
14 changes: 13 additions & 1 deletion src/builder/pod/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use k8s_openapi::api::core::v1::{
};
use std::fmt;

use crate::{error::Error, validation::is_rfc_1123_label};
use crate::{
commons::product_image_selection::ResolvedProductImage, error::Error,
validation::is_rfc_1123_label,
};

/// A builder to build [`Container`] objects.
///
Expand Down Expand Up @@ -45,6 +48,15 @@ impl ContainerBuilder {
self
}

/// Adds the following container attributes from a [ResolvedProductImage]:
/// * image
/// * image_pull_policy
pub fn image_from_product_image(&mut self, product_image: &ResolvedProductImage) -> &mut Self {
self.image = Some(product_image.image.clone());
self.image_pull_policy = Some(product_image.image_pull_policy.clone());
self
}

pub fn add_env_var(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.env.get_or_insert_with(Vec::new).push(EnvVar {
name: name.into(),
Expand Down
14 changes: 14 additions & 0 deletions src/builder/pod/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod security;
pub mod volume;

use crate::builder::meta::ObjectMetaBuilder;
use crate::commons::product_image_selection::ResolvedProductImage;
use crate::error::{Error, OperatorResult};

use k8s_openapi::{
Expand Down Expand Up @@ -290,6 +291,19 @@ impl PodBuilder {
self
}

/// Extend the pod's image_pull_secrets field with the pull secrets from a given [ResolvedProductImage]
pub fn image_pull_secrets_from_product_image(
&mut self,
product_image: &ResolvedProductImage,
) -> &mut Self {
if let Some(pull_secrets) = &product_image.pull_secrets {
self.image_pull_secrets
.get_or_insert_with(Vec::new)
.extend_from_slice(pull_secrets);
}
self
}

/// Hack because [`Pod`] predates [`LabelSelector`], and so its functionality is split between [`PodSpec::node_selector`] and [`Affinity::node_affinity`]
fn node_selector_for_label_selector(
label_selector: Option<LabelSelector>,
Expand Down
1 change: 1 addition & 0 deletions src/commons/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod authentication;
pub mod ldap;
pub mod listener;
pub mod opa;
pub mod product_image_selection;
pub mod resources;
pub mod s3;
pub mod secret_class;
Expand Down
313 changes: 313 additions & 0 deletions src/commons/product_image_selection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
use k8s_openapi::api::core::v1::LocalObjectReference;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::AsRefStr;

#[cfg(doc)]
use crate::labels::get_recommended_labels;

pub const STACKABLE_DOCKER_REPO: &str = "docker.stackable.tech/stackable";

#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProductImage {
#[serde(flatten)]
image_selection: ProductImageSelection,

#[serde(default)]
/// [Pull policy](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) used when pulling the Images
pull_policy: PullPolicy,

/// [Image pull secrets](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) to pull images from a private registry
pull_secrets: Option<Vec<LocalObjectReference>>,
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum ProductImageSelection {
// Order matters!
// The variants will be tried from top to bottom
Custom(ProductImageCustom),
StackableVersion(ProductImageStackableVersion),
}

#[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. `docker.stackable.tech/stackable/superset:1.4.1-stackable2.1.0`
custom: String,
/// Version of the product, e.g. `1.4.1`.
product_version: String,
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProductImageStackableVersion {
/// Version of the product, e.g. `1.4.1`.
product_version: String,
/// Stackable version of the product, e.g. 2.1.0
stackable_version: String,
/// Name of the docker repo, e.g. `docker.stackable.tech/stackable`
repo: Option<String>,
}

#[derive(Clone, Debug, PartialEq, JsonSchema)]
pub struct ResolvedProductImage {
/// Version of the product, e.g. `1.4.1`.
pub product_version: String,
/// App version as formatted for [`get_recommended_labels`]
pub app_version_label: String,
/// Image to be used for the product image e.g. `docker.stackable.tech/stackable/superset:1.4.1-stackable2.1.0`
pub image: String,
/// Image pull policy for the containers using the product image
pub image_pull_policy: String,
/// Image pull secrets for the containers using the product image
pub pull_secrets: Option<Vec<LocalObjectReference>>,
}

#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename = "PascalCase")]
#[derive(AsRefStr)]
pub enum PullPolicy {
IfNotPresent,
Always,
Never,
}

impl Default for PullPolicy {
fn default() -> PullPolicy {
PullPolicy::IfNotPresent
}
}

impl ProductImage {
pub fn resolve(&self, image_base_name: &str) -> ResolvedProductImage {
let image_pull_policy = self.pull_policy.as_ref().to_string();
let pull_secrets = self.pull_secrets.clone();

match &self.image_selection {
ProductImageSelection::Custom(custom) => {
let custom_image_tag = custom
.custom
.split_once(':')
.map_or("latest", |splits| splits.1);
let app_version_label = format!("{}-{}", custom.product_version, custom_image_tag);
ResolvedProductImage {
product_version: custom.product_version.to_string(),
app_version_label,
image: custom.custom.to_string(),
image_pull_policy,
pull_secrets,
}
}
ProductImageSelection::StackableVersion(stackable_version) => {
let repo = stackable_version
.repo
.as_deref()
.unwrap_or(STACKABLE_DOCKER_REPO);
let image = format!(
"{repo}/{image_base_name}:{product_version}-stackable{stackable_version}",
product_version = stackable_version.product_version,
stackable_version = stackable_version.stackable_version,
);
let app_version_label = format!(
"{product_version}-stackable{stackable_version}",
product_version = stackable_version.product_version,
stackable_version = stackable_version.stackable_version,
);
ResolvedProductImage {
product_version: stackable_version.product_version.to_string(),
app_version_label,
image,
image_pull_policy,
pull_secrets,
}
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

use rstest::rstest;

#[rstest]
#[case::stackable_version_without_repo(
"superset",
r#"
productVersion: 1.4.1
stackableVersion: 2.1.0
"#,
ResolvedProductImage {
image: "docker.stackable.tech/stackable/superset:1.4.1-stackable2.1.0".to_string(),
app_version_label: "1.4.1-stackable2.1.0".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
}
)]
#[case::stackable_version_with_repo(
"trino",
r#"
productVersion: 1.4.1
stackableVersion: 2.1.0
repo: my.corp/myteam/stackable
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/trino:1.4.1-stackable2.1.0".to_string(),
app_version_label: "1.4.1-stackable2.1.0".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
}
)]
#[case::custom_without_tag(
"superset",
r#"
custom: my.corp/myteam/stackable/superset
productVersion: 1.4.1
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset".to_string(),
app_version_label: "1.4.1-latest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
}
)]
#[case::custom_with_tag(
"superset",
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
productVersion: 1.4.1
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
app_version_label: "1.4.1-latest-and-greatest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
}
)]
#[case::custom_takes_precedence(
"superset",
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
productVersion: 1.4.1
stackableVersion: not-used
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
app_version_label: "1.4.1-latest-and-greatest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
}
)]
#[case::pull_policy_if_not_present(
"superset",
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
productVersion: 1.4.1
pullPolicy: IfNotPresent
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
app_version_label: "1.4.1-latest-and-greatest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "IfNotPresent".to_string(),
pull_secrets: None,
}
)]
#[case::pull_policy_always(
"superset",
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
productVersion: 1.4.1
pullPolicy: Always
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
app_version_label: "1.4.1-latest-and-greatest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "Always".to_string(),
pull_secrets: None,
}
)]
#[case::pull_policy_never(
"superset",
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
productVersion: 1.4.1
pullPolicy: Never
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
app_version_label: "1.4.1-latest-and-greatest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "Never".to_string(),
pull_secrets: None,
}
)]
#[case::pull_secrets(
"superset",
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
productVersion: 1.4.1
pullPolicy: Always
pullSecrets:
- name: myPullSecrets1
- name: myPullSecrets2
"#,
ResolvedProductImage {
image: "my.corp/myteam/stackable/superset:latest-and-greatest".to_string(),
app_version_label: "1.4.1-latest-and-greatest".to_string(),
product_version: "1.4.1".to_string(),
image_pull_policy: "Always".to_string(),
pull_secrets: Some(vec![LocalObjectReference{name: Some("myPullSecrets1".to_string())}, LocalObjectReference{name: Some("myPullSecrets2".to_string())}]),
}
)]
fn test_correct_resolved_image(
#[case] image_base_name: 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);

assert_eq!(resolved_product_image, expected);
}

#[rstest]
#[case::custom(
r#"
custom: my.corp/myteam/stackable/superset:latest-and-greatest
"#,
"data did not match any variant of untagged enum ProductImageSelection at line 2 column 9"
)]
#[case::product_version(
r#"
productVersion: 1.4.1
"#,
"data did not match any variant of untagged enum ProductImageSelection at line 2 column 9"
)]
#[case::stackable_version(
r#"
stackableVersion: 2.1.0
"#,
"data did not match any variant of untagged enum ProductImageSelection at line 2 column 9"
)]
#[case::empty(
"{}",
"data did not match any variant of untagged enum ProductImageSelection"
)]
fn test_invalid_image(#[case] input: String, #[case] expected: String) {
let err = serde_yaml::from_str::<ProductImage>(&input).expect_err("Must be error");

assert_eq!(err.to_string(), expected);
}
}
Loading