Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8d9d190
Initial commit
Techassi Oct 31, 2023
e59b225
Fix failing doc tests
Techassi Oct 31, 2023
2173dc5
Start to add more doc comments
Techassi Oct 31, 2023
b7b1dfb
Finish doc comments
Techassi Nov 2, 2023
f624afb
Add KeyValuePairs, add TryFrom<(&str, &str)> impl
Techassi Nov 2, 2023
5216c00
Add BTreeMap helper functions
Techassi Nov 3, 2023
215d57c
Add FromIterator impl
Techassi Nov 3, 2023
e9ffbe1
Start to add more test cases
Techassi Nov 3, 2023
5724407
Add invalid kvp tests
Techassi Nov 6, 2023
601b658
Add key, key prefix, and key name tests
Techassi Nov 6, 2023
23587bd
Add value tests
Techassi Nov 6, 2023
9d820af
Rename max length constants
Techassi Nov 6, 2023
88af1ab
Add associated function `push`
Techassi Nov 7, 2023
0ad1691
Rename unit tests
Techassi Nov 21, 2023
46c232e
Introduce generic type parameter to allow different value impls
Techassi Nov 22, 2023
fe45642
Fix annotation macro, add unit test
Techassi Nov 22, 2023
281232c
Rename generic type param, fix rustdoc error
Techassi Nov 22, 2023
a4e20aa
Add doc comments to macros
Techassi Nov 22, 2023
8c2dc0f
Update changelog
Techassi Nov 23, 2023
a95f3ac
Add macros and full feature
Techassi Nov 23, 2023
8e15d10
Start to change KeyValuePairs from Vec to HashSet
Techassi Nov 23, 2023
d164bae
Merge branch 'main' into feat/kvp
Techassi Nov 24, 2023
41510c0
Implement serde serialize
Techassi Nov 24, 2023
b5eed4b
Implement serde deserialize
Techassi Nov 24, 2023
d45854e
Use BTreeSet instead of HashSet for deterministic ordering
Techassi Nov 24, 2023
162e1d9
Add common func, add serde tests, fix deserialization
Techassi Nov 24, 2023
d6d2a7b
Introduce newtype Label and Annotation structs
Techassi Nov 28, 2023
bd8061a
Add recommended label function
Techassi Nov 28, 2023
7c46bcd
Start to adjust other label handling code (wip)
Techassi Nov 29, 2023
b19801c
Continue label/annotation handling code
Techassi Dec 4, 2023
366dd48
Add final fixes without breaking changes
Techassi Dec 5, 2023
d4bcc1f
Merge branch 'main' into feat/kvp
Techassi Dec 5, 2023
21eee91
Add label and annotation error type aliases
Techassi Dec 5, 2023
b0e74d8
Remove unwraps in secret volume builder
Techassi Dec 5, 2023
4380b05
Remove unwraps from listener volume builder
Techassi Dec 5, 2023
b52db5b
Remove unwraps in object meta builder
Techassi Dec 5, 2023
e4728a9
Remove unwraps in PDB builder
Techassi Dec 5, 2023
d127186
Rename trait ValueExt to Value
Techassi Dec 5, 2023
139b973
Change deref target
Techassi Dec 5, 2023
b3a897e
Simplify key parsing
Techassi Dec 6, 2023
28f73c8
Remove unneeded annotation validation
Techassi Dec 6, 2023
7cc8522
Remove FromStr impl for KeyValuePair, Label, and Annotation
Techassi Dec 6, 2023
be4d3a9
Remove unwraps from ClusterResources
Techassi Dec 6, 2023
99a09ee
Add annotation doc comments
Techassi Dec 11, 2023
df924e1
Add label doc comments
Techassi Dec 11, 2023
5fcec5a
Add module doc comment, remove outdated struct comment
Techassi Dec 11, 2023
5a0a789
Update const names, add doc comments
Techassi Dec 12, 2023
7e6279d
Remove label and annotation macro
Techassi Dec 12, 2023
cfaffd4
Add const module doc comment
Techassi Dec 12, 2023
6dcb465
Update and move label selector to query string functionality
Techassi Dec 12, 2023
007b028
Fix rustdoc error
Techassi Dec 12, 2023
5bf9873
Move workspace member back
Techassi Dec 14, 2023
1e6979f
Add doc comments to ObjectLabels struct
Techassi Dec 14, 2023
bd3814c
Add missing doc comments
Techassi Dec 14, 2023
bd2816c
Remove Deserialize and Serialize support
Techassi Dec 15, 2023
af7e522
Rework contains and add contains_key function
Techassi Dec 15, 2023
6f7375c
Merge branch 'main' into feat/kvp
Techassi Dec 15, 2023
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Add `stackble_operator::kvp` module and types to allow validated construction of key/value pairs, like labels and
annotations. Most users want to use the exported type aliases `Label` and `Annotation` ([#684]).

### Changed

- Move `stackable_operator::label_selector::convert_label_selector_to_query_string` into `kvp` module. The conversion
functionality now is encapsulated in a new trait `LabelSelectorExt`. An instance of a `LabelSelector` can now be
converted into a query string by calling the associated function `ls.to_query_string()` ([#684]).

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

## [0.58.1] - 2023-12-12

### Added
Expand Down
85 changes: 46 additions & 39 deletions src/builder/meta.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
use crate::error::{Error, OperatorResult};
use crate::labels::{self, ObjectLabels};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference};
use kube::{Resource, ResourceExt};
use std::collections::BTreeMap;
use snafu::{ResultExt, Snafu};
use tracing::warn;

use crate::{
error::{Error, OperatorResult},
kvp::{Annotation, Annotations, Label, LabelError, Labels, ObjectLabels},
};

// NOTE (Techassi): Think about that name
#[derive(Debug, Snafu)]
pub enum ObjectMetaBuilderError {
#[snafu(display("failed to set recommended labels"))]
RecommendedLabels { source: LabelError },
}

/// A builder to build [`ObjectMeta`] objects.
///
/// Of special interest is the [`Self::ownerreference_from_resource()`] function.
Expand All @@ -13,12 +23,12 @@ use tracing::warn;
/// It is strongly recommended to always call [`Self::with_recommended_labels()`]!
#[derive(Clone, Default)]
pub struct ObjectMetaBuilder {
name: Option<String>,
ownerreference: Option<OwnerReference>,
annotations: Option<Annotations>,
generate_name: Option<String>,
namespace: Option<String>,
ownerreference: Option<OwnerReference>,
labels: Option<BTreeMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
labels: Option<Labels>,
name: Option<String>,
}

impl ObjectMetaBuilder {
Expand Down Expand Up @@ -92,74 +102,70 @@ impl ObjectMetaBuilder {

/// This adds a single annotation to the existing annotations.
/// It'll override an annotation with the same key.
pub fn with_annotation(
&mut self,
annotation_key: impl Into<String>,
annotation_value: impl Into<String>,
) -> &mut Self {
pub fn with_annotation(&mut self, annotation: Annotation) -> &mut Self {
self.annotations
.get_or_insert_with(BTreeMap::new)
.insert(annotation_key.into(), annotation_value.into());
.get_or_insert(Annotations::new())
.insert(annotation);
self
}

/// This adds multiple annotations to the existing annotations.
/// Any existing annotation with a key that is contained in `annotations` will be overwritten
pub fn with_annotations(&mut self, annotations: BTreeMap<String, String>) -> &mut Self {
pub fn with_annotations(&mut self, annotations: Annotations) -> &mut Self {
self.annotations
.get_or_insert_with(BTreeMap::new)
.get_or_insert(Annotations::new())
.extend(annotations);
self
}

/// This will replace all existing annotations
pub fn annotations(&mut self, annotations: BTreeMap<String, String>) -> &mut Self {
pub fn annotations(&mut self, annotations: Annotations) -> &mut Self {
self.annotations = Some(annotations);
self
}

/// This adds a single label to the existing labels.
/// It'll override a label with the same key.
pub fn with_label(
&mut self,
label_key: impl Into<String>,
label_value: impl Into<String>,
) -> &mut Self {
self.labels
.get_or_insert_with(BTreeMap::new)
.insert(label_key.into(), label_value.into());
pub fn with_label(&mut self, label: Label) -> &mut Self {
self.labels.get_or_insert(Labels::new()).insert(label);
self
}

/// This adds multiple labels to the existing labels.
/// Any existing label with a key that is contained in `labels` will be overwritten
pub fn with_labels(&mut self, labels: BTreeMap<String, String>) -> &mut Self {
self.labels.get_or_insert_with(BTreeMap::new).extend(labels);
pub fn with_labels(&mut self, labels: Labels) -> &mut Self {
self.labels.get_or_insert(Labels::new()).extend(labels);
self
}

/// This will replace all existing labels
pub fn labels(&mut self, labels: BTreeMap<String, String>) -> &mut Self {
pub fn labels(&mut self, labels: Labels) -> &mut Self {
self.labels = Some(labels);
self
}

/// This sets the common recommended labels (in the `app.kubernetes.io` namespace).
/// It is recommended to always call this method.
/// The only reasons it is not _required_ is to make testing easier and to allow for more
/// flexibility if needed.
/// This sets the common recommended labels (in the `app.kubernetes.io`
/// namespace). It is recommended to always call this method. The only
/// reasons it is not _required_ is to make testing easier and to allow
/// for more flexibility if needed.
pub fn with_recommended_labels<T: Resource>(
&mut self,
object_labels: ObjectLabels<T>,
) -> &mut Self {
let recommended_labels = labels::get_recommended_labels(object_labels);
) -> Result<&mut Self, ObjectMetaBuilderError> {
let recommended_labels =
Labels::recommended(object_labels).context(RecommendedLabelsSnafu)?;

self.labels
.get_or_insert_with(BTreeMap::new)
.get_or_insert(Labels::new())
.extend(recommended_labels);
self

Ok(self)
}

pub fn build(&self) -> ObjectMeta {
// NOTE (Techassi): Shouldn't this take self instead of &self to consume
// the builder and build ObjectMeta without cloning?

// if 'generate_name' and 'name' are set, Kubernetes will prioritize the 'name' field and
// 'generate_name' has no impact.
if let (Some(name), Some(generate_name)) = (&self.name, &self.generate_name) {
Expand All @@ -178,8 +184,8 @@ impl ObjectMetaBuilder {
.ownerreference
.as_ref()
.map(|ownerreference| vec![ownerreference.clone()]),
labels: self.labels.clone(),
annotations: self.annotations.clone(),
labels: self.labels.clone().map(|l| l.into()),
annotations: self.annotations.clone().map(|a| a.into()),
..ObjectMeta::default()
}
}
Expand Down Expand Up @@ -329,7 +335,8 @@ mod tests {
role: "role",
role_group: "rolegroup",
})
.with_annotation("foo", "bar")
.unwrap()
.with_annotation(("foo", "bar").try_into().unwrap())
.build();

assert_eq!(meta.generate_name, Some("generate_foo".to_string()));
Expand Down
44 changes: 24 additions & 20 deletions src/builder/pdb.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
use crate::{
builder::ObjectMetaBuilder,
error::OperatorResult,
labels::{role_selector_labels, APP_MANAGED_BY_LABEL},
utils::format_full_controller_name,
};
use k8s_openapi::{
api::policy::v1::{PodDisruptionBudget, PodDisruptionBudgetSpec},
apimachinery::pkg::{
Expand All @@ -13,6 +7,12 @@ use k8s_openapi::{
};
use kube::{Resource, ResourceExt};

use crate::{
builder::ObjectMetaBuilder,
error::OperatorResult,
kvp::{Label, Labels},
};

/// This builder is used to construct [`PodDisruptionBudget`]s.
/// If you are using this to create [`PodDisruptionBudget`]s according to [ADR 30 on Allowed Pod disruptions][adr],
/// the use of [`PodDisruptionBudgetBuilder::new_with_role`] is recommended.
Expand Down Expand Up @@ -48,39 +48,43 @@ impl PodDisruptionBudgetBuilder<(), (), ()> {
PodDisruptionBudgetBuilder::default()
}

/// This method populates [`PodDisruptionBudget::metadata`] and [`PodDisruptionBudgetSpec::selector`] from the give role
/// (not roleGroup!).
/// This method populates [`PodDisruptionBudget::metadata`] and
/// [`PodDisruptionBudgetSpec::selector`] from the give role (not roleGroup!).
///
/// The parameters are the same as the fields from [`crate::labels::ObjectLabels`]:
/// * `owner` - Reference to the k8s object owning the created resource, such as `HdfsCluster` or `TrinoCluster`.
/// * `app_name` - The name of the app being managed, such as `hdfs` or `trino`.
/// * `role` - The role that this object belongs to, e.g. `datanode` or `worker`.
/// * `operator_name` - The DNS-style name of the operator managing the object (such as `hdfs.stackable.tech`).
/// * `controller_name` - The name of the controller inside of the operator managing the object (such as `hdfscluster`)
/// The parameters are the same as the fields from
/// [`ObjectLabels`][crate::kvp::ObjectLabels]:
///
/// * `owner` - Reference to the k8s object owning the created resource,
/// such as `HdfsCluster` or `TrinoCluster`.
/// * `app_name` - The name of the app being managed, such as `hdfs` or
/// `trino`.
/// * `role` - The role that this object belongs to, e.g. `datanode` or
/// `worker`.
/// * `operator_name` - The DNS-style name of the operator managing the
/// object (such as `hdfs.stackable.tech`).
/// * `controller_name` - The name of the controller inside of the operator
/// managing the object (such as `hdfscluster`)
pub fn new_with_role<T: Resource<DynamicType = ()>>(
owner: &T,
app_name: &str,
role: &str,
operator_name: &str,
controller_name: &str,
) -> OperatorResult<PodDisruptionBudgetBuilder<ObjectMeta, LabelSelector, ()>> {
let role_selector_labels = role_selector_labels(owner, app_name, role);
let role_selector_labels = Labels::role_selector(owner, app_name, role)?;
let metadata = ObjectMetaBuilder::new()
.namespace_opt(owner.namespace())
.name(format!("{}-{}", owner.name_any(), role))
.ownerreference_from_resource(owner, None, Some(true))?
.with_labels(role_selector_labels.clone())
.with_label(
APP_MANAGED_BY_LABEL.to_string(),
format_full_controller_name(operator_name, controller_name),
)
.with_label(Label::managed_by(operator_name, controller_name)?)
.build();

Ok(PodDisruptionBudgetBuilder {
metadata,
selector: LabelSelector {
match_expressions: None,
match_labels: Some(role_selector_labels),
match_labels: Some(role_selector_labels.into()),
},
..PodDisruptionBudgetBuilder::default()
})
Expand Down
80 changes: 45 additions & 35 deletions src/builder/pod/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
pub mod container;
pub mod resources;
pub mod security;
pub mod volume;
use std::{collections::BTreeMap, num::TryFromIntError};

use std::collections::BTreeMap;
use std::num::TryFromIntError;
use k8s_openapi::{
api::core::v1::{
Affinity, Container, LocalObjectReference, NodeAffinity, Pod, PodAffinity, PodAntiAffinity,
PodCondition, PodSecurityContext, PodSpec, PodStatus, PodTemplateSpec,
ResourceRequirements, Toleration, Volume,
},
apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::ObjectMeta},
};
use snafu::{ResultExt, Snafu};
use tracing::warn;

use crate::{
builder::{
meta::ObjectMetaBuilder, ListenerOperatorVolumeSourceBuilder, ListenerReference,
VolumeBuilder,
meta::ObjectMetaBuilder, ListenerOperatorVolumeSourceBuilder,
ListenerOperatorVolumeSourceBuilderError, ListenerReference, VolumeBuilder,
},
commons::{
affinity::StackableAffinity,
Expand All @@ -23,23 +28,24 @@ use crate::{
time::Duration,
};

use k8s_openapi::{
api::core::v1::{
Affinity, Container, LocalObjectReference, NodeAffinity, Pod, PodAffinity, PodAntiAffinity,
PodCondition, PodSecurityContext, PodSpec, PodStatus, PodTemplateSpec,
ResourceRequirements, Toleration, Volume,
},
apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::ObjectMeta},
};
use tracing::warn;
pub mod container;
pub mod resources;
pub mod security;
pub mod volume;

#[derive(Debug, thiserror::Error)]
#[derive(Debug, Snafu)]
pub enum Error {
#[error("termination grace period is too long (got {duration}, maximum allowed is {max})", max = Duration::from_secs(i64::MAX as u64))]
#[snafu(display("termination grace period is too long (got {duration}, maximum allowed is {max})", max = Duration::from_secs(i64::MAX as u64)))]
TerminationGracePeriodTooLong {
source: TryFromIntError,
duration: Duration,
},

#[snafu(display("failed to add listener volume '{name}' to the pod"))]
ListenerVolume {
source: ListenerOperatorVolumeSourceBuilderError,
name: String,
},
}
pub type Result<T> = std::result::Result<T, Error>;

Expand Down Expand Up @@ -299,6 +305,7 @@ impl PodBuilder {
/// .build(),
/// )
/// .add_listener_volume_by_listener_class("listener", "nodeport")
/// .unwrap()
/// .build()
/// .unwrap();
///
Expand Down Expand Up @@ -341,18 +348,19 @@ impl PodBuilder {
&mut self,
volume_name: &str,
listener_class: &str,
) -> &mut Self {
) -> Result<&mut Self> {
let listener_reference = ListenerReference::ListenerClass(listener_class.to_string());
let volume = ListenerOperatorVolumeSourceBuilder::new(&listener_reference)
.build()
.context(ListenerVolumeSnafu { name: volume_name })?;

self.add_volume(Volume {
name: volume_name.into(),
ephemeral: Some(
ListenerOperatorVolumeSourceBuilder::new(&ListenerReference::ListenerClass(
listener_class.into(),
))
.build(),
),
ephemeral: Some(volume),
..Volume::default()
});
self

Ok(self)
}

/// Add a [`Volume`] for the storage class `listeners.stackable.tech` with the given listener
Expand Down Expand Up @@ -386,6 +394,7 @@ impl PodBuilder {
/// .build(),
/// )
/// .add_listener_volume_by_listener_name("listener", "preprovisioned-listener")
/// .unwrap()
/// .build()
/// .unwrap();
///
Expand Down Expand Up @@ -428,18 +437,19 @@ impl PodBuilder {
&mut self,
volume_name: &str,
listener_name: &str,
) -> &mut Self {
) -> Result<&mut Self> {
let listener_reference = ListenerReference::ListenerName(listener_name.to_string());
let volume = ListenerOperatorVolumeSourceBuilder::new(&listener_reference)
.build()
.context(ListenerVolumeSnafu { name: volume_name })?;

self.add_volume(Volume {
name: volume_name.into(),
ephemeral: Some(
ListenerOperatorVolumeSourceBuilder::new(&ListenerReference::ListenerName(
listener_name.into(),
))
.build(),
),
ephemeral: Some(volume),
..Volume::default()
});
self

Ok(self)
}

pub fn image_pull_secrets(
Expand Down
Loading