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
36 changes: 36 additions & 0 deletions src/builder/kvp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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> {
pub fn new<T>(prefix: Option<T>) -> Self
where
T: Into<String>,
{
Self {
prefix: prefix.map(Into::into),
kvps: Vec::new(),
}
}

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)
}

pub fn build(self) -> BTreeMap<String, P> {
self.kvps.iter().map(|a| (a.key(), a.clone())).collect()
}
}
42 changes: 24 additions & 18 deletions src/builder/meta.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::error::{Error, OperatorResult};
use crate::labels::{self, ObjectLabels};
use crate::types::Annotation;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ObjectMeta, OwnerReference};
use kube::{Resource, ResourceExt};
use std::collections::BTreeMap;
Expand All @@ -18,7 +19,7 @@ pub struct ObjectMetaBuilder {
namespace: Option<String>,
ownerreference: Option<OwnerReference>,
labels: Option<BTreeMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
annotations: BTreeMap<String, Annotation>,
}

impl ObjectMetaBuilder {
Expand Down Expand Up @@ -92,29 +93,29 @@ 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) -> &mut 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: Vec<Annotation>) -> &mut Self {
for annotation in annotations {
self.annotations.insert(annotation.key(), annotation);
}
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: Vec<Annotation>) -> &mut Self {
let mut map = BTreeMap::new();

for annotation in annotations {
map.insert(annotation.key(), annotation);
}

self.annotations = map;
self
}

Expand Down Expand Up @@ -179,7 +180,12 @@ impl ObjectMetaBuilder {
.as_ref()
.map(|ownerreference| vec![ownerreference.clone()]),
labels: self.labels.clone(),
annotations: self.annotations.clone(),
annotations: Some(
self.annotations
.iter()
.map(|(name, annotation)| (name.clone(), annotation.value()))
Techassi marked this conversation as resolved.
Show resolved Hide resolved
Techassi marked this conversation as resolved.
Show resolved Hide resolved
.collect(),
),
..ObjectMeta::default()
}
}
Expand Down Expand Up @@ -305,7 +311,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 +335,7 @@ mod tests {
role: "role",
role_group: "rolegroup",
})
.with_annotation("foo", "bar")
.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 @@ -3,6 +3,7 @@
//! They are often not _pure_ builders but contain extra logic to set fields based on others or
//! to fill in sensible defaults.
//!
pub mod annotation;
Techassi marked this conversation as resolved.
Show resolved Hide resolved
Techassi marked this conversation as resolved.
Show resolved Hide resolved
pub mod configmap;
pub mod event;
pub mod meta;
Expand Down
6 changes: 2 additions & 4 deletions src/builder/pod/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ use k8s_openapi::{
use tracing::warn;

use crate::{
commons::resources::ResourceRequirementsType, cpu::CpuQuantity, error::OperatorResult,
memory::MemoryQuantity,
commons::resources::ResourceRequirementsType, constants::resources::RESOURCE_DENYLIST,
cpu::CpuQuantity, error::OperatorResult, memory::MemoryQuantity,
};

const RESOURCE_DENYLIST: &[&str] = &["cpu", "memory"];

#[derive(Debug, Default)]
pub struct ResourceRequirementsBuilder<CR, CL, MR, ML> {
other: BTreeMap<String, BTreeMap<ResourceRequirementsType, Quantity>>,
Expand Down
48 changes: 22 additions & 26 deletions src/builder/pod/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use k8s_openapi::{
},
apimachinery::pkg::api::resource::Quantity,
};
use std::collections::BTreeMap;

use crate::builder::ObjectMetaBuilder;
use crate::builder::{annotation::AnnotationListBuilder, ObjectMetaBuilder};
Techassi marked this conversation as resolved.
Show resolved Hide resolved
Techassi marked this conversation as resolved.
Show resolved Hide resolved
use crate::types::{Annotation, AnnotationParseError};

/// A builder to build [`Volume`] objects.
/// May only contain one `volume_source` at a time.
Expand Down Expand Up @@ -306,10 +306,9 @@ impl SecretOperatorVolumeSourceBuilder {
}

pub fn build(&self) -> EphemeralVolumeSource {
let mut attrs = BTreeMap::from([(
"secrets.stackable.tech/class".to_string(),
self.secret_class.clone(),
)]);
// TODO (Techassi): Get rid of all unwraps
let mut attrs = AnnotationListBuilder::new(Some("secrets.stackable.tech"));
attrs.add("class", &self.secret_class).unwrap();

if !self.scopes.is_empty() {
let mut scopes = String::new();
Expand All @@ -326,26 +325,25 @@ impl SecretOperatorVolumeSourceBuilder {
}
}
}
attrs.insert("secrets.stackable.tech/scope".to_string(), scopes);
attrs.add("scope", &scopes).unwrap();
}

if let Some(format) = &self.format {
attrs.insert(
"secrets.stackable.tech/format".to_string(),
format.as_ref().to_string(),
);
attrs.add("format", format.as_ref()).unwrap();
}

if !self.kerberos_service_names.is_empty() {
attrs.insert(
"secrets.stackable.tech/kerberos.service.names".to_string(),
self.kerberos_service_names.join(","),
);
attrs
.add(
"kerberos.service.names",
&self.kerberos_service_names.join(","),
)
.unwrap();
}

EphemeralVolumeSource {
volume_claim_template: Some(PersistentVolumeClaimTemplate {
metadata: Some(ObjectMetaBuilder::new().annotations(attrs).build()),
metadata: Some(ObjectMetaBuilder::new().annotations(attrs.build()).build()),
spec: PersistentVolumeClaimSpec {
storage_class_name: Some("secrets.stackable.tech".to_string()),
resources: Some(ResourceRequirements {
Expand Down Expand Up @@ -390,16 +388,14 @@ pub enum ListenerReference {

impl ListenerReference {
/// Return the key and value for a Kubernetes object annotation
fn to_annotation(&self) -> (String, String) {
fn to_annotation(&self) -> Result<Annotation, AnnotationParseError> {
match self {
ListenerReference::ListenerClass(value) => (
"listeners.stackable.tech/listener-class".into(),
value.into(),
),
ListenerReference::ListenerName(value) => (
"listeners.stackable.tech/listener-name".into(),
value.into(),
),
ListenerReference::ListenerClass(value) => {
Annotation::new(Some("listeners.stackable.tech"), "listener-class", value)
}
ListenerReference::ListenerName(value) => {
Annotation::new(Some("listeners.stackable.tech"), "listener-name", value)
}
}
}
}
Expand Down Expand Up @@ -449,7 +445,7 @@ impl ListenerOperatorVolumeSourceBuilder {
volume_claim_template: Some(PersistentVolumeClaimTemplate {
metadata: Some(
ObjectMetaBuilder::new()
.annotations([self.listener_reference.to_annotation()].into())
.annotations([self.listener_reference.to_annotation().unwrap()].into())
.build(),
),
spec: PersistentVolumeClaimSpec {
Expand Down
2 changes: 0 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ use std::{
path::{Path, PathBuf},
};

pub const AUTHOR: &str = "Stackable GmbH - info@stackable.de";

/// Framework-standardized commands
///
/// If you need operator-specific commands then you can flatten [`Command`] into your own command enum. For example:
Expand Down
2 changes: 1 addition & 1 deletion src/cluster_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
LIMIT_REQUEST_RATIO_CPU, LIMIT_REQUEST_RATIO_MEMORY,
},
},
constants::labels::{APP_INSTANCE_LABEL, APP_MANAGED_BY_LABEL, APP_NAME_LABEL},
Techassi marked this conversation as resolved.
Show resolved Hide resolved
Techassi marked this conversation as resolved.
Show resolved Hide resolved
error::{Error, OperatorResult},
k8s_openapi::{
api::{
Expand All @@ -24,7 +25,6 @@ use crate::{
NamespaceResourceScope,
},
kube::{Resource, ResourceExt},
labels::{APP_INSTANCE_LABEL, APP_MANAGED_BY_LABEL, APP_NAME_LABEL},
utils::format_full_controller_name,
};

Expand Down
7 changes: 4 additions & 3 deletions src/commons/affinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ use stackable_operator_derive::Fragment;

use crate::{
config::merge::{Atomic, Merge},
labels::{APP_COMPONENT_LABEL, APP_INSTANCE_LABEL, APP_NAME_LABEL},
constants::{
affinity::TOPOLOGY_KEY_HOSTNAME,
labels::{APP_COMPONENT_LABEL, APP_INSTANCE_LABEL, APP_NAME_LABEL},
Techassi marked this conversation as resolved.
Show resolved Hide resolved
Techassi marked this conversation as resolved.
Show resolved Hide resolved
},
};

pub const TOPOLOGY_KEY_HOSTNAME: &str = "kubernetes.io/hostname";

#[derive(Clone, Debug, Default, Deserialize, Fragment, JsonSchema, PartialEq, Serialize)]
#[fragment(path_overrides(fragment = "crate::config::fragment"))]
#[fragment_attrs(
Expand Down
1 change: 1 addition & 0 deletions src/constants/affinity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const TOPOLOGY_KEY_HOSTNAME: &str = "kubernetes.io/hostname";
1 change: 1 addition & 0 deletions src/constants/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const AUTHOR: &str = "Stackable GmbH - info@stackable.de";
26 changes: 26 additions & 0 deletions src/constants/labels.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// The label key prefix used for Kubernetes apps
pub const LABEL_KEY_PREFIX_APP_KUBERNETES: &str = "app.kubernetes.io";

/// The label key name identifying the tool used to manage the operation of an
/// application, e.g. "helm"
pub const LABEL_KEY_NAME_APP_MANAGED_BY: &str = "managed-by";

pub const LABEL_KEY_NAME_APP_ROLE_GROUP: &str = "role-group";

/// The label key name identifying the application component within the
/// architecture, e.g. "database"
pub const LABEL_KEY_NAME_APP_COMPONENT: &str = "component";

/// The label key name identifying the application instance, e.g. "mysql-abcxzy"
pub const LABEL_KEY_NAME_APP_INSTANCE: &str = "instance";

/// The label key name identifying the application version, e.g. a semantic
/// version, revision hash, etc, like "5.7.21"
pub const LABEL_KEY_NAME_APP_VERSION: &str = "version";

/// The label key name identifying the higher level application this app is part
/// of, e.g. "wordpress".
pub const LABEL_KEY_NAME_APP_PART_OF: &str = "part-of";

/// The label key name identifying the application name e.g. "mysql"
pub const LABEL_KEY_NAME_APP_NAME: &str = "name";
5 changes: 5 additions & 0 deletions src/constants/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod affinity;
pub mod cli;
pub mod labels;
pub mod resources;
pub mod validation;
1 change: 1 addition & 0 deletions src/constants/resources.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) const RESOURCE_DENYLIST: &[&str] = &["cpu", "memory"];
18 changes: 18 additions & 0 deletions src/constants/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use const_format::concatcp;

pub(crate) const RFC_1123_LABEL_FMT: &str = "[a-z0-9]([-a-z0-9]*[a-z0-9])?";
pub(crate) const RFC_1123_SUBDOMAIN_FMT: &str =
concatcp!(RFC_1123_LABEL_FMT, "(\\.", RFC_1123_LABEL_FMT, ")*");
pub(crate) const RFC_1123_SUBDOMAIN_ERROR_MSG: &str = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";
pub(crate) const RFC_1123_LABEL_ERROR_MSG: &str = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character";

// This is a subdomain's max length in DNS (RFC 1123)
pub const RFC_1123_SUBDOMAIN_MAX_LENGTH: usize = 253;
// Minimal length reuquired by RFC 1123 is 63. Up to 255 allowed, unsupported by k8s.
pub const RFC_1123_LABEL_MAX_LENGTH: usize = 63;

pub(crate) const RFC_1035_LABEL_FMT: &str = "[a-z]([-a-z0-9]*[a-z0-9])?";
pub(crate) const RFC_1035_LABEL_ERR_MSG: &str = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character";

// This is a label's max length in DNS (RFC 1035)
pub const RFC_1035_LABEL_MAX_LENGTH: usize = RFC_1123_LABEL_MAX_LENGTH;
Loading
Loading