Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Add key/value pair validation #615

Closed
wants to merge 10 commits into from
73 changes: 73 additions & 0 deletions src/builder/kvp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use std::collections::BTreeMap;

use crate::types::{Annotation, KeyValuePairExt, KeyValuePairParseError, Label};

pub type AnnotationListBuilder = KeyValuePairBuilder<Annotation>;
pub type LabelListBuilder = KeyValuePairBuilder<Label>;

pub struct KeyValuePairBuilder<P: KeyValuePairExt> {
prefix: Option<String>,
kvps: Vec<P>,
}

impl<P: KeyValuePairExt> KeyValuePairBuilder<P> {
/// Creates a new key/value pair list builder with items of type `P`. The
/// optional `prefix` will be attached to each added pair via the
/// [`KeyValuePairBuilder::add`] method. This builder makes it easier to
/// build a list of key/value pairs which all share a **common** key prefix,
/// like `app.kubernetes.io/<name>=<value>`.
pub fn new<T>(prefix: Option<T>) -> Self
where
T: Into<String>,
{
Self {
prefix: prefix.map(Into::into),
kvps: Vec::new(),
}
}

/// Tries to add a new key/value pair to this builder. This method ensures
/// that the optional key prefix is valid and in addition also validates
/// the key name. Both values must not exceed the maximum length of 253
/// characters for the prefix and 63 characters for the name. Also, both
/// need to only contain allowed characters.
pub fn add<T>(&mut self, name: T, value: T) -> Result<&mut Self, KeyValuePairParseError>
where
T: Into<String>,
{
let kvp = P::new(self.prefix.clone(), name.into(), value.into())?;
self.kvps.push(kvp);
Ok(self)
}

/// Builds a [`BTreeMap<String, P>`] out of the added pairs. This mthods is
/// useful when we need a map of items with the underlying type `P` still
/// preserved. The Kubernetes API uses [`BTreeMap<String, String>`] to
/// handle lists of labels and annotations. The build such a map, use the
/// `build_raw` method instead.
pub fn build(self) -> BTreeMap<String, P> {
self.kvps.into_iter().map(|kvp| (kvp.key(), kvp)).collect()
}

/// Builds a [`BTreeMap<String, String>`] out of the added pairs. This
/// method is useful when the returned value is directly passed to
/// the Kubernetes API or data structures.
pub fn build_raw(self) -> BTreeMap<String, String> {
self.kvps
.into_iter()
.map(|kvp| (kvp.key(), kvp.value().clone()))
.collect()
}
}

pub trait KeyValuePairMapExt {
fn into_raw(self) -> BTreeMap<String, String>;
}

impl<P: KeyValuePairExt> KeyValuePairMapExt for BTreeMap<String, P> {
fn into_raw(self) -> BTreeMap<String, String> {
self.into_iter()
.map(|(name, kvp)| (name, kvp.value().clone()))
.collect()
}
}
114 changes: 54 additions & 60 deletions src/builder/meta.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use crate::error::{Error, OperatorResult};
use crate::labels::{self, ObjectLabels};
use std::collections::BTreeMap;

use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference};
use kube::{Resource, ResourceExt};
use std::collections::BTreeMap;
use tracing::warn;

use crate::{
builder::kvp::KeyValuePairMapExt,
error::{Error, OperatorResult},
labels::{self, ObjectLabels},
types::{Annotation, KeyValuePairExt, Label, LabelParseError},
};

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

impl ObjectMetaBuilder {
Expand All @@ -27,60 +33,60 @@ impl ObjectMetaBuilder {
}

/// This sets the name and namespace from a given resource
pub fn name_and_namespace<T: Resource>(&mut self, resource: &T) -> &mut Self {
pub fn name_and_namespace<T: Resource>(mut self, resource: &T) -> Self {
self.name = Some(resource.name_any());
self.namespace = resource.namespace();
self
}

pub fn name_opt(&mut self, name: impl Into<Option<String>>) -> &mut Self {
pub fn name_opt(mut self, name: impl Into<Option<String>>) -> Self {
self.name = name.into();
self
}

pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}

pub fn generate_name(&mut self, generate_name: impl Into<String>) -> &mut Self {
pub fn generate_name(mut self, generate_name: impl Into<String>) -> Self {
self.generate_name = Some(generate_name.into());
self
}

pub fn generate_name_opt(&mut self, generate_name: impl Into<Option<String>>) -> &mut Self {
pub fn generate_name_opt(mut self, generate_name: impl Into<Option<String>>) -> Self {
self.generate_name = generate_name.into();
self
}

pub fn namespace_opt(&mut self, namespace: impl Into<Option<String>>) -> &mut Self {
pub fn namespace_opt(mut self, namespace: impl Into<Option<String>>) -> Self {
self.namespace = namespace.into();
self
}

pub fn namespace(&mut self, namespace: impl Into<String>) -> &mut Self {
pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}

pub fn ownerreference(&mut self, ownerreference: OwnerReference) -> &mut Self {
self.ownerreference = Some(ownerreference);
pub fn ownerreference(mut self, ownerreference: OwnerReference) -> Self {
self.owner_reference = Some(ownerreference);
self
}

pub fn ownerreference_opt(&mut self, ownerreference: Option<OwnerReference>) -> &mut Self {
self.ownerreference = ownerreference;
pub fn ownerreference_opt(mut self, ownerreference: Option<OwnerReference>) -> Self {
self.owner_reference = ownerreference;
self
}

/// This can be used to set the `OwnerReference` to the provided resource.
pub fn ownerreference_from_resource<T: Resource<DynamicType = ()>>(
&mut self,
mut self,
resource: &T,
block_owner_deletion: Option<bool>,
controller: Option<bool>,
) -> OperatorResult<&mut Self> {
self.ownerreference = Some(
) -> OperatorResult<Self> {
self.owner_reference = Some(
OwnerReferenceBuilder::new()
.initialize_from_resource(resource)
.block_owner_deletion_opt(block_owner_deletion)
Expand All @@ -92,55 +98,41 @@ 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 {
self.annotations
.get_or_insert_with(BTreeMap::new)
.insert(annotation_key.into(), annotation_value.into());
pub fn with_annotation(mut self, annotation: Annotation) -> Self {
self.annotations.insert(annotation.key(), 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 {
self.annotations
.get_or_insert_with(BTreeMap::new)
.extend(annotations);
pub fn with_annotations(mut self, annotations: BTreeMap<String, Annotation>) -> Self {
self.annotations.extend(annotations);
self
}

/// This will replace all existing annotations
pub fn annotations(&mut self, annotations: BTreeMap<String, String>) -> &mut Self {
self.annotations = Some(annotations);
pub fn annotations(mut self, annotations: BTreeMap<String, Annotation>) -> Self {
self.annotations = 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) -> Self {
self.labels.insert(label.key(), 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: BTreeMap<String, Label>) -> Self {
self.labels.extend(labels);
self
}

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

Expand All @@ -149,17 +141,15 @@ impl ObjectMetaBuilder {
/// 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,
mut self,
object_labels: ObjectLabels<T>,
) -> &mut Self {
let recommended_labels = labels::get_recommended_labels(object_labels);
self.labels
.get_or_insert_with(BTreeMap::new)
.extend(recommended_labels);
self
) -> Result<Self, LabelParseError> {
let recommended_labels = labels::get_recommended_labels(object_labels)?;
self.labels.extend(recommended_labels);
Ok(self)
}

pub fn build(&self) -> ObjectMeta {
pub fn build(self) -> ObjectMeta {
// 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 @@ -170,16 +160,19 @@ impl ObjectMetaBuilder {
);
}

let annotations = (!self.annotations.is_empty()).then_some(self.annotations.into_raw());
let labels = (!self.labels.is_empty()).then_some(self.labels.into_raw());

ObjectMeta {
generate_name: self.generate_name.clone(),
name: self.name.clone(),
namespace: self.namespace.clone(),
owner_references: self
.ownerreference
.owner_reference
.as_ref()
.map(|ownerreference| vec![ownerreference.clone()]),
labels: self.labels.clone(),
annotations: self.annotations.clone(),
labels,
annotations,
..ObjectMeta::default()
}
}
Expand Down Expand Up @@ -305,7 +298,7 @@ impl OwnerReferenceBuilder {
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::meta::ObjectMetaBuilder;
use crate::{builder::meta::ObjectMetaBuilder, types::Annotation};
use k8s_openapi::api::core::v1::Pod;

#[test]
Expand All @@ -329,7 +322,8 @@ mod tests {
role: "role",
role_group: "rolegroup",
})
.with_annotation("foo", "bar")
.unwrap()
.with_annotation(Annotation::new(None, "foo", "bar").unwrap())
.build();

assert_eq!(meta.generate_name, Some("generate_foo".to_string()));
Expand Down
1 change: 1 addition & 0 deletions src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
pub mod configmap;
pub mod event;
pub mod kvp;
pub mod meta;
pub mod pod;

Expand Down
3 changes: 2 additions & 1 deletion src/builder/pod/container.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::fmt;

use k8s_openapi::api::core::v1::{
ConfigMapKeySelector, Container, ContainerPort, EnvVar, EnvVarSource, ObjectFieldSelector,
Probe, ResourceRequirements, SecretKeySelector, SecurityContext, VolumeMount,
};
use std::fmt;

use crate::{
commons::product_image_selection::ResolvedProductImage, error::Error,
Expand Down
Loading