diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 12f3da0..50448f6 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -41,6 +41,7 @@ spec: maxLength: 253 minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: object clusterOperation: @@ -191,6 +192,7 @@ spec: maxLength: 253 minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string logging: default: @@ -539,6 +541,7 @@ spec: maxLength: 253 minLength: 1 nullable: true + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string logging: default: diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 832e488..3a6d210 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -33,10 +33,16 @@ use crate::{ v1alpha1::{self}, }, framework::{ - ClusterName, ControllerName, HasName, HasUid, ListenerClassName, NameIsValidLabelValue, - NamespaceName, OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, Uid, + HasName, HasUid, NameIsValidLabelValue, product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig}, role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, + types::{ + kubernetes::{ListenerClassName, NamespaceName, Uid}, + operator::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, + RoleGroupName, RoleName, + }, + }, }, }; @@ -380,10 +386,13 @@ mod tests { controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig}, crd::{NodeRoles, v1alpha1}, framework::{ - ClusterName, ListenerClassName, NamespaceName, OperatorName, ProductVersion, - RoleGroupName, builder::pod::container::EnvVarSet, + builder::pod::container::EnvVarSet, product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, + types::{ + kubernetes::{ListenerClassName, NamespaceName}, + operator::{ClusterName, OperatorName, ProductVersion, RoleGroupName}, + }, }, }; diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index 49afd02..1365e68 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -10,7 +10,13 @@ use stackable_operator::{ use strum::{EnumDiscriminants, IntoStaticStr}; use super::{Applied, ContextNames, KubernetesResources, Prepared}; -use crate::framework::{ClusterName, NamespaceName, Uid, cluster_resources::cluster_resources_new}; +use crate::framework::{ + cluster_resources::cluster_resources_new, + types::{ + kubernetes::{NamespaceName, Uid}, + operator::ClusterName, + }, +}; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 43c3bda..08a1afb 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -79,9 +79,15 @@ mod tests { }, crd::{NodeRoles, v1alpha1}, framework::{ - ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, - ProductName, ProductVersion, RoleGroupName, builder::pod::container::EnvVarSet, + builder::pod::container::EnvVarSet, role_utils::GenericProductSpecificCommonConfig, + types::{ + kubernetes::{ListenerClassName, NamespaceName}, + operator::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, + RoleGroupName, + }, + }, }, }; diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 9249042..6a1aa4b 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -10,9 +10,9 @@ use crate::{ controller::OpenSearchRoleGroupConfig, crd::v1alpha1, framework::{ - RoleGroupName, ServiceName, builder::pod::container::{EnvVarName, EnvVarSet}, role_group_utils, + types::{kubernetes::ServiceName, operator::RoleGroupName}, }, }; @@ -296,9 +296,12 @@ mod tests { controller::{ValidatedLogging, ValidatedOpenSearchConfig}, crd::NodeRoles, framework::{ - ClusterName, ListenerClassName, NamespaceName, ProductVersion, RoleGroupName, product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, + types::{ + kubernetes::{ListenerClassName, NamespaceName}, + operator::{ClusterName, ProductVersion, RoleGroupName}, + }, }, }; diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 454d7d5..f556909 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -241,9 +241,15 @@ mod tests { }, crd::{NodeRoles, v1alpha1}, framework::{ - ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, - ProductName, ProductVersion, RoleGroupName, builder::pod::container::EnvVarSet, + builder::pod::container::EnvVarSet, role_utils::GenericProductSpecificCommonConfig, + types::{ + kubernetes::{ListenerClassName, NamespaceName}, + operator::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, + RoleGroupName, + }, + }, }, }; diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 896c344..8d8880f 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -42,7 +42,6 @@ use crate::{ }, crd::v1alpha1, framework::{ - PersistentVolumeClaimName, RoleGroupName, ServiceAccountName, ServiceName, VolumeName, builder::{ meta::ownerreference_from_resource, pod::{ @@ -55,6 +54,10 @@ use crate::{ STACKABLE_LOG_DIR, ValidatedContainerLogConfigChoice, vector_container, }, role_group_utils::ResourceNames, + types::{ + kubernetes::{PersistentVolumeClaimName, ServiceAccountName, ServiceName, VolumeName}, + operator::RoleGroupName, + }, }, }; @@ -663,11 +666,19 @@ mod tests { }, crd::{NodeRoles, v1alpha1}, framework::{ - ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, - OperatorName, ProductName, ProductVersion, RoleGroupName, ServiceAccountName, - ServiceName, builder::pod::container::EnvVarSet, + builder::pod::container::EnvVarSet, product_logging::framework::VectorContainerLogConfig, role_utils::GenericProductSpecificCommonConfig, + types::{ + kubernetes::{ + ConfigMapName, ListenerClassName, NamespaceName, ServiceAccountName, + ServiceName, + }, + operator::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, + RoleGroupName, + }, + }, }, }; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index c23ff48..0f6f10c 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -18,12 +18,15 @@ use super::{ use crate::{ crd::v1alpha1::{self}, framework::{ - ClusterName, ConfigMapName, NamespaceName, Uid, builder::pod::container::{EnvVarName, EnvVarSet}, product_logging::framework::{ VectorContainerLogConfig, validate_logging_configuration_for_container, }, role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig, with_validated_config}, + types::{ + kubernetes::{ConfigMapName, NamespaceName, Uid}, + operator::ClusterName, + }, }, }; @@ -45,13 +48,19 @@ pub enum Error { GetVectorAggregatorConfigMapName {}, #[snafu(display("failed to set cluster name"))] - ParseClusterName { source: crate::framework::Error }, + ParseClusterName { + source: crate::framework::macros::attributed_string_type::Error, + }, #[snafu(display("failed to set cluster namespace"))] - ParseClusterNamespace { source: crate::framework::Error }, + ParseClusterNamespace { + source: crate::framework::macros::attributed_string_type::Error, + }, #[snafu(display("failed to set UID"))] - ParseClusterUid { source: crate::framework::Error }, + ParseClusterUid { + source: crate::framework::macros::attributed_string_type::Error, + }, #[snafu(display("failed to parse environment variable"))] ParseEnvironmentVariable { @@ -59,10 +68,14 @@ pub enum Error { }, #[snafu(display("failed to set product version"))] - ParseProductVersion { source: crate::framework::Error }, + ParseProductVersion { + source: crate::framework::macros::attributed_string_type::Error, + }, #[snafu(display("failed to set role-group name"))] - ParseRoleGroupName { source: crate::framework::Error }, + ParseRoleGroupName { + source: crate::framework::macros::attributed_string_type::Error, + }, #[snafu(display("failed to resolve product image"))] ResolveProductImage { @@ -277,13 +290,18 @@ mod tests { v1alpha1::{self}, }, framework::{ - ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, - OperatorName, ProductName, ProductVersion, RoleGroupName, builder::pod::container::{EnvVarName, EnvVarSet}, product_logging::framework::{ ValidatedContainerLogConfigChoice, VectorContainerLogConfig, }, role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, + types::{ + kubernetes::{ConfigMapName, ListenerClassName, NamespaceName}, + operator::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, + RoleGroupName, + }, + }, }, }; diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 4803e70..768ea11 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -29,8 +29,12 @@ use strum::{Display, EnumIter}; use crate::{ constant, framework::{ - ClusterName, ConfigMapName, ContainerName, ListenerClassName, NameIsValidLabelValue, - ProductName, RoleName, role_utils::GenericProductSpecificCommonConfig, + NameIsValidLabelValue, + role_utils::GenericProductSpecificCommonConfig, + types::{ + kubernetes::{ConfigMapName, ContainerName, ListenerClassName}, + operator::{ClusterName, ProductName, RoleName}, + }, }, }; diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 3442b2e..2e42c3c 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -20,64 +20,16 @@ //! become less frequent, then this module can be incorporated into stackable-operator. The module //! structure should already resemble the one of stackable-operator. -use std::str::FromStr; - -use snafu::Snafu; -use stackable_operator::validation::{ - RFC_1035_LABEL_MAX_LENGTH, RFC_1123_LABEL_MAX_LENGTH, RFC_1123_SUBDOMAIN_MAX_LENGTH, -}; -use strum::{EnumDiscriminants, IntoStaticStr}; +use types::kubernetes::Uid; pub mod builder; pub mod cluster_resources; pub mod kvp; +pub mod macros; pub mod product_logging; pub mod role_group_utils; pub mod role_utils; -pub mod validation; - -#[derive(Debug, EnumDiscriminants, Snafu)] -#[strum_discriminants(derive(IntoStaticStr))] -pub enum Error { - #[snafu(display("empty strings are not allowed"))] - EmptyString {}, - - #[snafu(display("maximum length exceeded"))] - LengthExceeded { length: usize, max_length: usize }, - - #[snafu(display("not a valid ConfigMap key"))] - InvalidConfigMapKey { - source: crate::framework::validation::Error, - }, - - #[snafu(display("not a valid label value"))] - InvalidLabelValue { - source: stackable_operator::kvp::LabelValueError, - }, - - #[snafu(display("not a valid label name as defined in RFC 1035"))] - InvalidRfc1035LabelName { - source: stackable_operator::validation::Errors, - }, - - #[snafu(display("not a valid DNS subdomain name as defined in RFC 1123"))] - InvalidRfc1123DnsSubdomainName { - source: stackable_operator::validation::Errors, - }, - - #[snafu(display("not a valid label name as defined in RFC 1123"))] - InvalidRfc1123LabelName { - source: stackable_operator::validation::Errors, - }, - - #[snafu(display("not a valid UUID"))] - InvalidUid { source: uuid::Error }, -} - -/// Maximum length of label values -/// -/// Duplicates the private constant [`stackable-operator::kvp::label::value::LABEL_VALUE_MAX_LEN`] -pub const MAX_LABEL_VALUE_LENGTH: usize = 63; +pub mod types; /// Has a non-empty name /// @@ -97,742 +49,3 @@ pub trait HasUid { pub trait NameIsValidLabelValue { fn to_label_value(&self) -> String; } - -/// Restricted string type with attributes like maximum length. -/// -/// Fully-qualified types are used to ease the import into other modules. -/// -/// # Examples -/// -/// ```rust -/// attributed_string_type! { -/// ConfigMapName, -/// "The name of a ConfigMap", -/// "opensearch-nodes-default", -/// (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), -/// is_rfc_1123_dns_subdomain_name -/// } -/// ``` -#[macro_export(local_inner_macros)] -macro_rules! attributed_string_type { - ($name:ident, $description:literal, $example:literal $(, $attribute:tt)*) => { - #[doc = std::concat!($description, ", e.g. \"", $example, "\"")] - #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] - pub struct $name(String); - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl From<$name> for String { - fn from(value: $name) -> Self { - value.0 - } - } - - impl From<&$name> for String { - fn from(value: &$name) -> Self { - value.0.clone() - } - } - - impl AsRef for $name { - fn as_ref(&self) -> &str { - &self.0 - } - } - - impl std::str::FromStr for $name { - type Err = $crate::framework::Error; - - fn from_str(s: &str) -> std::result::Result { - // ResultExt::context is used on most but not all usages of this macro - #[allow(unused_imports)] - use snafu::ResultExt; - - snafu::ensure!( - !s.is_empty(), - $crate::framework::EmptyStringSnafu {} - ); - - $(attributed_string_type!(@from_str $name, s, $attribute);)* - - Ok(Self(s.to_owned())) - } - } - - impl<'de> serde::Deserialize<'de> for $name { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let string: String = serde::Deserialize::deserialize(deserializer)?; - $name::from_str(&string).map_err(|err| serde::de::Error::custom(&err)) - } - } - - impl serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.0.serialize(serializer) - } - } - - impl stackable_operator::config::merge::Atomic for $name {} - - #[cfg(test)] - impl $name { - #[allow(dead_code)] - pub fn from_str_unsafe(s: &str) -> Self { - std::str::FromStr::from_str(s).expect("should be a valid {name}") - } - - // A dead_code warning is emitted if there is no unit test that calls this function. - pub fn test_example() { - Self::from_str_unsafe($example); - } - } - - $(attributed_string_type!(@trait_impl $name, $attribute);)* - }; - (@from_str $name:ident, $s:expr, (max_length = $max_length:expr)) => { - let length = $s.len() as usize; - snafu::ensure!( - length <= $name::MAX_LENGTH, - $crate::framework::LengthExceededSnafu { - length, - max_length: $name::MAX_LENGTH, - } - ); - }; - (@from_str $name:ident, $s:expr, is_config_map_key) => { - $crate::framework::validation::is_config_map_key($s).context($crate::framework::InvalidConfigMapKeySnafu)?; - }; - (@from_str $name:ident, $s:expr, is_rfc_1035_label_name) => { - stackable_operator::validation::is_lowercase_rfc_1035_label($s).context($crate::framework::InvalidRfc1035LabelNameSnafu)?; - }; - (@from_str $name:ident, $s:expr, is_rfc_1123_dns_subdomain_name) => { - stackable_operator::validation::is_lowercase_rfc_1123_subdomain($s).context($crate::framework::InvalidRfc1123DnsSubdomainNameSnafu)?; - }; - (@from_str $name:ident, $s:expr, is_rfc_1123_label_name) => { - stackable_operator::validation::is_lowercase_rfc_1123_label($s).context($crate::framework::InvalidRfc1123LabelNameSnafu)?; - }; - (@from_str $name:ident, $s:expr, is_valid_label_value) => { - stackable_operator::kvp::LabelValue::from_str($s).context($crate::framework::InvalidLabelValueSnafu)?; - }; - (@from_str $name:ident, $s:expr, is_uid) => { - uuid::Uuid::try_parse($s).context($crate::framework::InvalidUidSnafu)?; - }; - (@trait_impl $name:ident, (max_length = $max_length:expr)) => { - impl $name { - // type arithmetic would be better - pub const MAX_LENGTH: usize = $max_length; - } - - // The JsonSchema implementation requires `max_length`. - impl schemars::JsonSchema for $name { - fn schema_name() -> std::borrow::Cow<'static, str> { - std::stringify!($name).into() - } - - fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { - schemars::json_schema!({ - "type": "string", - "minLength": 1, - "maxLength": $name::MAX_LENGTH - }) - } - } - }; - (@trait_impl $name:ident, is_config_map_key) => { - }; - (@trait_impl $name:ident, is_rfc_1035_label_name) => { - impl $name { - pub const IS_RFC_1035_LABEL_NAME: bool = true; - pub const IS_RFC_1123_LABEL_NAME: bool = true; - pub const IS_RFC_1123_SUBDOMAIN_NAME: bool = true; - } - }; - (@trait_impl $name:ident, is_rfc_1123_dns_subdomain_name) => { - impl $name { - pub const IS_RFC_1123_SUBDOMAIN_NAME: bool = true; - } - }; - (@trait_impl $name:ident, is_rfc_1123_label_name) => { - impl $name { - pub const IS_RFC_1123_LABEL_NAME: bool = true; - pub const IS_RFC_1123_SUBDOMAIN_NAME: bool = true; - } - }; - (@trait_impl $name:ident, is_uid) => { - impl From for $name { - fn from(value: uuid::Uuid) -> Self { - Self(value.to_string()) - } - } - - impl From<&uuid::Uuid> for $name { - fn from(value: &uuid::Uuid) -> Self { - Self(value.to_string()) - } - } - }; - (@trait_impl $name:ident, is_valid_label_value) => { - impl $name { - pub const IS_VALID_LABEL_VALUE: bool = true; - } - - impl $crate::framework::NameIsValidLabelValue for $name { - fn to_label_value(&self) -> String { - self.0.clone() - } - } - }; -} - -/// Use [`std::sync::LazyLock`] to define a static "constant" from a string. -/// -/// The string is converted into the given type with [`std::str::FromStr::from_str`]. -/// -/// # Examples -/// -/// ```rust -/// constant!(DATA_VOLUME_NAME: VolumeName = "data"); -/// constant!(pub CONFIG_VOLUME_NAME: VolumeName = "config"); -/// ``` -#[macro_export(local_inner_macros)] -macro_rules! constant { - ($qualifier:vis $name:ident: $type:ident = $value:literal) => { - $qualifier static $name: std::sync::LazyLock<$type> = - std::sync::LazyLock::new(|| $type::from_str($value).expect("should be a valid $name")); - }; -} - -/// Returns the minimum of the given values. -/// -/// As opposed to [`std::cmp::min`], this function can be used at compile-time. -/// -/// # Examples -/// -/// ```rust -/// assert_eq!(2, min(2, 3)); -/// assert_eq!(4, min(5, 4)); -/// assert_eq!(1, min(1, 1)); -/// ``` -pub const fn min(x: usize, y: usize) -> usize { - if x < y { x } else { y } -} - -// Kubernetes (resource) names - -attributed_string_type! { - ConfigMapName, - "The name of a ConfigMap", - "opensearch-nodes-default", - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - ConfigMapKey, - "The key for a ConfigMap or Secret", - "log4j2.properties", - // see https://github.com/kubernetes/kubernetes/blob/v1.34.1/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L435-L451 - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_config_map_key -} -attributed_string_type! { - ContainerName, - "The name of a container in a Pod", - "opensearch", - (max_length = RFC_1123_LABEL_MAX_LENGTH), - is_rfc_1123_label_name -} -attributed_string_type! { - ClusterRoleName, - "The name of a ClusterRole", - "opensearch-clusterrole", - // On the one hand, ClusterRoles must only contain characters that are allowed for DNS - // subdomain names, on the other hand, their length does not seem to be restricted – at least - // on Kind. However, 253 characters are sufficient for the Stackable operators, and to avoid - // problems on other Kubernetes providers, the length is restricted here. - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - ListenerName, - "The name of a Listener", - "opensearch-nodes-default", - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - ListenerClassName, - "The name of a Listener", - "external-stable", - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - NamespaceName, - "The name of a Namespace", - "stackable-operators", - (max_length = min(RFC_1123_LABEL_MAX_LENGTH, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1123_label_name, - is_valid_label_value -} -attributed_string_type! { - PersistentVolumeClaimName, - "The name of a PersistentVolumeClaim", - "config", - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - RoleBindingName, - "The name of a RoleBinding", - "opensearch-rolebinding", - // On the one hand, RoleBindings must only contain characters that are allowed for DNS - // subdomain names, on the other hand, their length does not seem to be restricted – at least - // on Kind. However, 253 characters are sufficient for the Stackable operators, and to avoid - // problems on other Kubernetes providers, the length is restricted here. - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - ServiceAccountName, - "The name of a ServiceAccount", - "opensearch-serviceaccount", - (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), - is_rfc_1123_dns_subdomain_name -} -attributed_string_type! { - ServiceName, - "The name of a Service", - "opensearch-nodes-default-headless", - (max_length = min(RFC_1035_LABEL_MAX_LENGTH, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1035_label_name, - is_valid_label_value -} -attributed_string_type! { - StatefulSetName, - "The name of a StatefulSet", - "opensearch-nodes-default", - (max_length = min( - // see https://github.com/kubernetes/kubernetes/issues/64023 - RFC_1123_LABEL_MAX_LENGTH - - 1 /* dash */ - - 10 /* digits for the controller-revision-hash label */, - MAX_LABEL_VALUE_LENGTH)), - is_rfc_1123_label_name, - is_valid_label_value -} -attributed_string_type! { - Uid, - "A UID", - "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", - (max_length = min(uuid::fmt::Hyphenated::LENGTH, MAX_LABEL_VALUE_LENGTH)), - is_uid, - is_valid_label_value -} -attributed_string_type! { - VolumeName, - "The name of a Volume", - "opensearch-nodes-default", - (max_length = min(RFC_1123_LABEL_MAX_LENGTH, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1123_label_name, - is_valid_label_value -} - -// Operator names - -attributed_string_type! { - ProductName, - "The name of a product", - "opensearch", - // A suffix is added to produce a label value. An according compile-time check ensures that - // max_length cannot be set higher. - (max_length = min(54, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1123_dns_subdomain_name, - is_valid_label_value -} -attributed_string_type! { - ProductVersion, - "The version of a product", - "3.1.0", - (max_length = MAX_LABEL_VALUE_LENGTH), - is_valid_label_value -} -attributed_string_type! { - ClusterName, - "The name of a cluster/stacklet", - "my-opensearch-cluster", - // Suffixes are added to produce resource names. According compile-time checks ensure that - // max_length cannot be set higher. - (max_length = min(24, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1035_label_name, - is_valid_label_value -} -attributed_string_type! { - ControllerName, - "The name of a controller in an operator", - "opensearchcluster", - (max_length = MAX_LABEL_VALUE_LENGTH), - is_valid_label_value -} -attributed_string_type! { - OperatorName, - "The name of an operator", - "opensearch.stackable.tech", - (max_length = MAX_LABEL_VALUE_LENGTH), - is_valid_label_value -} -attributed_string_type! { - RoleGroupName, - "The name of a role-group name", - "cluster-manager", - // The role-group name is used to produce resource names. To make sure that all resource names - // are valid, max_length is restricted. Compile-time checks ensure that max_length cannot be - // set higher if not other names like the RoleName are set lower accordingly. - (max_length = min(16, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1123_label_name, - is_valid_label_value -} -attributed_string_type! { - RoleName, - "The name of a role name", - "nodes", - // The role name is used to produce resource names. To make sure that all resource names are - // valid, max_length is restricted. Compile-time checks ensure that max_length cannot be set - // higher if not other names like the RoleGroupName are set lower accordingly. - (max_length = min(10, MAX_LABEL_VALUE_LENGTH)), - is_rfc_1123_label_name, - is_valid_label_value -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use schemars::{JsonSchema, SchemaGenerator}; - use serde_json::{Number, Value, json}; - use uuid::uuid; - - use super::{ - ClusterName, ClusterRoleName, ConfigMapKey, ConfigMapName, ContainerName, ControllerName, - ErrorDiscriminants, ListenerClassName, ListenerName, NamespaceName, OperatorName, - PersistentVolumeClaimName, ProductVersion, RoleBindingName, RoleGroupName, RoleName, - ServiceAccountName, ServiceName, StatefulSetName, Uid, VolumeName, - }; - use crate::framework::{NameIsValidLabelValue, ProductName}; - - #[test] - fn test_attributed_string_type_examples() { - ConfigMapName::test_example(); - ConfigMapKey::test_example(); - ContainerName::test_example(); - ClusterRoleName::test_example(); - ListenerName::test_example(); - ListenerClassName::test_example(); - NamespaceName::test_example(); - PersistentVolumeClaimName::test_example(); - RoleBindingName::test_example(); - ServiceAccountName::test_example(); - ServiceName::test_example(); - StatefulSetName::test_example(); - Uid::test_example(); - VolumeName::test_example(); - - ProductName::test_example(); - ProductVersion::test_example(); - ClusterName::test_example(); - ControllerName::test_example(); - OperatorName::test_example(); - RoleGroupName::test_example(); - RoleName::test_example(); - } - - attributed_string_type! { - DisplayFmtTest, - "Display::fmt test", - "test" - } - - #[test] - fn test_attributed_string_type_display_fmt() { - type T = DisplayFmtTest; - - assert_eq!("test", format!("{}", T::from_str_unsafe("test"))); - } - - attributed_string_type! { - StringFromTest, - "String::from test", - "test" - } - - #[test] - fn test_attributed_string_type_string_from() { - type T = StringFromTest; - - T::test_example(); - assert_eq!("test", String::from(T::from_str_unsafe("test"))); - assert_eq!("test", String::from(&T::from_str_unsafe("test"))); - } - - attributed_string_type! { - LengthTest, - "empty string and max_length test", - "test", - (max_length = 4) - } - - #[test] - fn test_attributed_string_type_length() { - type T = LengthTest; - - T::test_example(); - assert_eq!(4, T::MAX_LENGTH); - assert_eq!( - Err(ErrorDiscriminants::EmptyString), - T::from_str("").map_err(ErrorDiscriminants::from) - ); - assert_eq!( - Err(ErrorDiscriminants::LengthExceeded), - T::from_str("testX").map_err(ErrorDiscriminants::from) - ); - } - - attributed_string_type! { - JsonSchemaTest, - "JsonSchemaTest test", - "test", - (max_length = 4) - } - - #[test] - fn test_attributed_string_type_json_schema() { - type T = JsonSchemaTest; - - T::test_example(); - assert_eq!("JsonSchemaTest", JsonSchemaTest::schema_name()); - assert_eq!( - json!({ - "type": "string", - "minLength": 1, - "maxLength": 4 - }), - JsonSchemaTest::json_schema(&mut SchemaGenerator::default()) - ); - } - - attributed_string_type! { - SerializeTest, - "serde::Serialize test", - "test" - } - - #[test] - fn test_attributed_string_type_serialize() { - type T = SerializeTest; - - T::test_example(); - assert_eq!( - "\"test\"".to_owned(), - serde_json::to_string(&T::from_str_unsafe("test")).expect("should be serializable") - ); - } - - attributed_string_type! { - DeserializeTest, - "serde::Deserialize test", - "test", - (max_length = 4), - is_rfc_1123_label_name - } - - #[test] - fn test_attributed_string_type_deserialize() { - type T = DeserializeTest; - - T::test_example(); - assert_eq!( - T::from_str_unsafe("test"), - serde_json::from_value(Value::String("test".to_owned())) - .expect("should be deserializable") - ); - assert_eq!( - Err("empty strings are not allowed".to_owned()), - serde_json::from_value::(Value::String("".to_owned())) - .map_err(|err| err.to_string()) - ); - assert_eq!( - Err("maximum length exceeded".to_owned()), - serde_json::from_value::(Value::String("testx".to_owned())) - .map_err(|err| err.to_string()) - ); - assert_eq!( - Err("not a valid label name as defined in RFC 1123".to_owned()), - serde_json::from_value::(Value::String("-".to_owned())) - .map_err(|err| err.to_string()) - ); - assert_eq!( - Err("invalid type: null, expected a string".to_owned()), - serde_json::from_value::(Value::Null).map_err(|err| err.to_string()) - ); - assert_eq!( - Err("invalid type: boolean `true`, expected a string".to_owned()), - serde_json::from_value::(Value::Bool(true)).map_err(|err| err.to_string()) - ); - assert_eq!( - Err("invalid type: integer `1`, expected a string".to_owned()), - serde_json::from_value::(Value::Number( - Number::from_i128(1).expect("should be a valid number") - )) - .map_err(|err| err.to_string()) - ); - assert_eq!( - Err("invalid type: sequence, expected a string".to_owned()), - serde_json::from_value::(Value::Array(vec![])).map_err(|err| err.to_string()) - ); - assert_eq!( - Err("invalid type: map, expected a string".to_owned()), - serde_json::from_value::(Value::Object(serde_json::Map::new())) - .map_err(|err| err.to_string()) - ); - } - - attributed_string_type! { - IsConfigMapKeyTest, - "is_config_map_key test", - "a_B-c.1", - is_config_map_key - } - - #[test] - fn test_attributed_string_type_is_config_map_key() { - type T = IsConfigMapKeyTest; - - T::test_example(); - assert_eq!( - Err(ErrorDiscriminants::InvalidConfigMapKey), - T::from_str(" ").map_err(ErrorDiscriminants::from) - ); - } - - attributed_string_type! { - IsRfc1035LabelNameTest, - "is_rfc_1035_label_name test", - "a-b", - is_rfc_1035_label_name - } - - #[test] - fn test_attributed_string_type_is_rfc_1035_label_name() { - type T = IsRfc1035LabelNameTest; - - let _ = T::IS_RFC_1035_LABEL_NAME; - let _ = T::IS_RFC_1123_LABEL_NAME; - let _ = T::IS_RFC_1123_SUBDOMAIN_NAME; - - T::test_example(); - assert_eq!( - Err(ErrorDiscriminants::InvalidRfc1035LabelName), - T::from_str("A").map_err(ErrorDiscriminants::from) - ); - } - - attributed_string_type! { - IsRfc1123DnsSubdomainNameTest, - "is_rfc_1123_dns_subdomain_name test", - "a-b.c", - is_rfc_1123_dns_subdomain_name - } - - #[test] - fn test_attributed_string_type_is_rfc_1123_dns_subdomain_name() { - type T = IsRfc1123DnsSubdomainNameTest; - - let _ = T::IS_RFC_1123_SUBDOMAIN_NAME; - - T::test_example(); - assert_eq!( - Err(ErrorDiscriminants::InvalidRfc1123DnsSubdomainName), - T::from_str("A").map_err(ErrorDiscriminants::from) - ); - } - - attributed_string_type! { - IsRfc1123LabelNameTest, - "is_rfc_1123_label_name test", - "1-a", - is_rfc_1123_label_name - } - - #[test] - fn test_attributed_string_type_is_rfc_1123_label_name() { - type T = IsRfc1123LabelNameTest; - - let _ = T::IS_RFC_1123_LABEL_NAME; - let _ = T::IS_RFC_1123_SUBDOMAIN_NAME; - - T::test_example(); - assert_eq!( - Err(ErrorDiscriminants::InvalidRfc1123LabelName), - T::from_str("A").map_err(ErrorDiscriminants::from) - ); - } - - attributed_string_type! { - IsValidLabelValueTest, - "is_valid_label_value test", - "a-_.1", - is_valid_label_value - } - - #[test] - fn test_attributed_string_type_is_valid_label_value() { - type T = IsValidLabelValueTest; - - let _ = T::IS_VALID_LABEL_VALUE; - - T::test_example(); - assert_eq!( - Err(ErrorDiscriminants::InvalidLabelValue), - T::from_str("invalid label value").map_err(ErrorDiscriminants::from) - ); - assert_eq!( - "label-value", - T::from_str_unsafe("label-value").to_label_value() - ); - } - - attributed_string_type! { - IsUidTest, - "is_uid test", - "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", - is_uid - } - - #[test] - fn test_attributed_string_type_is_uid() { - type T = IsUidTest; - - T::test_example(); - assert_eq!( - Err(ErrorDiscriminants::InvalidUid), - T::from_str("invalid UID").map_err(ErrorDiscriminants::from) - ); - assert_eq!( - "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", - T::from(uuid!("c27b3971-ca72-42c1-80a4-abdfc1db0ddd")).to_string() - ); - assert_eq!( - "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", - T::from(&uuid!("c27b3971-ca72-42c1-80a4-abdfc1db0ddd")).to_string() - ); - } -} diff --git a/rust/operator-binary/src/framework/builder/pdb.rs b/rust/operator-binary/src/framework/builder/pdb.rs index b45383e..b9c7fdc 100644 --- a/rust/operator-binary/src/framework/builder/pdb.rs +++ b/rust/operator-binary/src/framework/builder/pdb.rs @@ -5,7 +5,8 @@ use stackable_operator::{ }; use crate::framework::{ - ControllerName, HasName, HasUid, NameIsValidLabelValue, OperatorName, ProductName, RoleName, + HasName, HasUid, NameIsValidLabelValue, + types::operator::{ControllerName, OperatorName, ProductName, RoleName}, }; /// Infallible variant of @@ -46,8 +47,12 @@ mod tests { }; use crate::framework::{ - ControllerName, HasName, HasUid, NameIsValidLabelValue, OperatorName, ProductName, - RoleName, Uid, builder::pdb::pod_disruption_budget_builder_with_role, + HasName, HasUid, NameIsValidLabelValue, + builder::pdb::pod_disruption_budget_builder_with_role, + types::{ + kubernetes::Uid, + operator::{ControllerName, OperatorName, ProductName, RoleName}, + }, }; struct Cluster { diff --git a/rust/operator-binary/src/framework/builder/pod/container.rs b/rust/operator-binary/src/framework/builder/pod/container.rs index a7a8ced..244bf00 100644 --- a/rust/operator-binary/src/framework/builder/pod/container.rs +++ b/rust/operator-binary/src/framework/builder/pod/container.rs @@ -7,7 +7,7 @@ use stackable_operator::{ }; use strum::{EnumDiscriminants, IntoStaticStr}; -use crate::framework::{ConfigMapKey, ConfigMapName, ContainerName}; +use crate::framework::types::kubernetes::{ConfigMapKey, ConfigMapName, ContainerName}; #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] @@ -187,7 +187,8 @@ mod tests { use super::{EnvVarName, EnvVarSet}; use crate::framework::{ - ConfigMapKey, ConfigMapName, ContainerName, builder::pod::container::new_container_builder, + builder::pod::container::new_container_builder, + types::kubernetes::{ConfigMapKey, ConfigMapName, ContainerName}, }; #[test] diff --git a/rust/operator-binary/src/framework/builder/pod/volume.rs b/rust/operator-binary/src/framework/builder/pod/volume.rs index ea1121f..06dc484 100644 --- a/rust/operator-binary/src/framework/builder/pod/volume.rs +++ b/rust/operator-binary/src/framework/builder/pod/volume.rs @@ -3,7 +3,9 @@ use stackable_operator::{ k8s_openapi::api::core::v1::PersistentVolumeClaim, kvp::Labels, }; -use crate::framework::{ListenerClassName, ListenerName, PersistentVolumeClaimName}; +use crate::framework::types::kubernetes::{ + ListenerClassName, ListenerName, PersistentVolumeClaimName, +}; /// Infallible variant of [`stackable_operator::builder::pod::volume::ListenerReference`] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/rust/operator-binary/src/framework/cluster_resources.rs b/rust/operator-binary/src/framework/cluster_resources.rs index b218e53..a57c9fa 100644 --- a/rust/operator-binary/src/framework/cluster_resources.rs +++ b/rust/operator-binary/src/framework/cluster_resources.rs @@ -3,8 +3,13 @@ use stackable_operator::{ k8s_openapi::api::core::v1::ObjectReference, }; -use super::{ClusterName, ControllerName, NamespaceName, OperatorName, ProductName, Uid}; -use crate::framework::{MAX_LABEL_VALUE_LENGTH, NameIsValidLabelValue}; +use super::types::{ + kubernetes::{NamespaceName, Uid}, + operator::{ClusterName, ControllerName, OperatorName, ProductName}, +}; +use crate::framework::{ + NameIsValidLabelValue, macros::attributed_string_type::MAX_LABEL_VALUE_LENGTH, +}; /// Infallible variant of [`stackable_operator::cluster_resources::ClusterResources::new`] pub fn cluster_resources_new( diff --git a/rust/operator-binary/src/framework/kvp/label.rs b/rust/operator-binary/src/framework/kvp/label.rs index 7f87e0f..918e69a 100644 --- a/rust/operator-binary/src/framework/kvp/label.rs +++ b/rust/operator-binary/src/framework/kvp/label.rs @@ -4,8 +4,10 @@ use stackable_operator::{ }; use crate::framework::{ - ControllerName, HasName, NameIsValidLabelValue, OperatorName, ProductName, ProductVersion, - RoleGroupName, RoleName, + HasName, NameIsValidLabelValue, + types::operator::{ + ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, + }, }; /// Infallible variant of [`stackable_operator::kvp::Labels::recommended`] @@ -72,9 +74,11 @@ mod tests { }; use crate::framework::{ - ControllerName, HasName, NameIsValidLabelValue, OperatorName, ProductName, ProductVersion, - RoleGroupName, RoleName, + HasName, NameIsValidLabelValue, kvp::label::{recommended_labels, role_group_selector, role_selector}, + types::operator::{ + ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, + }, }; struct Cluster { diff --git a/rust/operator-binary/src/framework/macros.rs b/rust/operator-binary/src/framework/macros.rs new file mode 100644 index 0000000..c25def9 --- /dev/null +++ b/rust/operator-binary/src/framework/macros.rs @@ -0,0 +1,2 @@ +pub mod attributed_string_type; +pub mod constant; diff --git a/rust/operator-binary/src/framework/macros/attributed_string_type.rs b/rust/operator-binary/src/framework/macros/attributed_string_type.rs new file mode 100644 index 0000000..f961712 --- /dev/null +++ b/rust/operator-binary/src/framework/macros/attributed_string_type.rs @@ -0,0 +1,920 @@ +use snafu::Snafu; +use strum::{EnumDiscriminants, IntoStaticStr}; + +/// Maximum length of label values +/// +/// Duplicates the private constant [`stackable-operator::kvp::label::value::LABEL_VALUE_MAX_LEN`] +pub const MAX_LABEL_VALUE_LENGTH: usize = 63; + +#[derive(Debug, EnumDiscriminants, Snafu)] +#[snafu(visibility(pub))] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("minimum length not met"))] + MinimumLengthNotMet { length: usize, min_length: usize }, + + #[snafu(display("maximum length exceeded"))] + LengthExceeded { length: usize, max_length: usize }, + + #[snafu(display("invalid regular expression"))] + InvalidRegex { source: regex::Error }, + + #[snafu(display("regular expression not matched"))] + RegexNotMatched { value: String, regex: &'static str }, + + #[snafu(display("not a valid label value"))] + InvalidLabelValue { + source: stackable_operator::kvp::LabelValueError, + }, + + #[snafu(display("not a valid label name as defined in RFC 1035"))] + InvalidRfc1035LabelName { + source: stackable_operator::validation::Errors, + }, + + #[snafu(display("not a valid DNS subdomain name as defined in RFC 1123"))] + InvalidRfc1123DnsSubdomainName { + source: stackable_operator::validation::Errors, + }, + + #[snafu(display("not a valid label name as defined in RFC 1123"))] + InvalidRfc1123LabelName { + source: stackable_operator::validation::Errors, + }, + + #[snafu(display("not a valid UUID"))] + InvalidUid { source: uuid::Error }, +} + +/// Helper data type to determine combined regular expressions +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Regex { + /// There is a regular expression but it is unknown (because it was too complicated to + /// calculate it). + Unknown, + + /// `MatchAll` equals `Expression(".*")`, but `MatchAll` can be pattern matched in a const + /// context, whereas `Expression(...)` cannot. + MatchAll, + + /// A regular expression + Expression(&'static str), +} + +impl Regex { + /// Combine this regular expression with the given one. + pub const fn combine(self, other: Regex) -> Regex { + match (self, other) { + (_, Regex::MatchAll) => self, + (Regex::MatchAll, _) => other, + // It is hard to combine two regular expressions and nearly impossible to do this in a + // const context. Fortunately, for most of the data types, only one regular expression + // is set. + _ => Regex::Unknown, + } + } +} + +/// Restricted string type with attributes like maximum length. +/// +/// Fully-qualified types are used to ease the import into other modules. +/// +/// # Examples +/// +/// ```rust +/// attributed_string_type! { +/// ConfigMapName, +/// "The name of a ConfigMap", +/// "opensearch-nodes-default", +/// is_rfc_1123_dns_subdomain_name +/// } +/// ``` +#[macro_export(local_inner_macros)] +macro_rules! attributed_string_type { + ($name:ident, $description:literal, $example:literal $(, $attribute:tt)*) => { + #[doc = std::concat!($description, ", e.g. \"", $example, "\"")] + #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] + pub struct $name(String); + + impl $name { + /// The minimum length + pub const MIN_LENGTH: usize = attributed_string_type!(@min_length $($attribute)*); + + /// The maximum length + pub const MAX_LENGTH: usize = attributed_string_type!(@max_length $($attribute)*); + + /// The regular expression + /// + /// This field is not meant to be used outside of this macro. + pub const REGEX: $crate::framework::macros::attributed_string_type::Regex = attributed_string_type!(@regex $($attribute)*); + } + + impl stackable_operator::config::merge::Atomic for $name {} + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl From<$name> for String { + fn from(value: $name) -> Self { + value.0 + } + } + + impl From<&$name> for String { + fn from(value: &$name) -> Self { + value.0.clone() + } + } + + impl AsRef for $name { + fn as_ref(&self) -> &str { + &self.0 + } + } + + impl std::str::FromStr for $name { + type Err = $crate::framework::macros::attributed_string_type::Error; + + fn from_str(s: &str) -> std::result::Result { + // ResultExt::context is used on most but not all usages of this macro + #[allow(unused_imports)] + use snafu::ResultExt; + + $(attributed_string_type!(@from_str $name, s, $attribute);)* + + Ok(Self(s.to_owned())) + } + } + + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string: String = serde::Deserialize::deserialize(deserializer)?; + $name::from_str(&string).map_err(|err| serde::de::Error::custom(&err)) + } + } + + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } + } + + // The JsonSchema implementation requires `max_length`. + impl schemars::JsonSchema for $name { + fn schema_name() -> std::borrow::Cow<'static, str> { + std::stringify!($name).into() + } + + fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "type": "string", + "minLength": $name::MIN_LENGTH, + "maxLength": if $name::MAX_LENGTH != usize::MAX { + Some($name::MAX_LENGTH) + } else { + // Do not set maxLength if it is usize::MAX. + None + }, + "pattern": match $name::REGEX { + $crate::framework::macros::attributed_string_type::Regex::Expression(regex) => Some(std::format!("^{regex}$")), + _ => None + } + }) + } + } + + #[cfg(test)] + impl $name { + #[allow(dead_code)] + pub fn from_str_unsafe(s: &str) -> Self { + std::str::FromStr::from_str(s).expect("should be a valid {name}") + } + + // A dead_code warning is emitted if there is no unit test that calls this function. + pub fn test_example() { + Self::from_str_unsafe($example); + } + } + + $(attributed_string_type!(@trait_impl $name, $attribute);)* + }; + + // std::str::FromStr + + (@from_str $name:ident, $s:expr, (min_length = $min_length:expr)) => { + let length = $s.len() as usize; + snafu::ensure!( + length >= $name::MIN_LENGTH, + $crate::framework::macros::attributed_string_type::MinimumLengthNotMetSnafu { + length, + min_length: $name::MIN_LENGTH, + } + ); + }; + (@from_str $name:ident, $s:expr, (max_length = $max_length:expr)) => { + let length = $s.len() as usize; + snafu::ensure!( + length <= $name::MAX_LENGTH, + $crate::framework::macros::attributed_string_type::LengthExceededSnafu { + length, + max_length: $name::MAX_LENGTH, + } + ); + }; + (@from_str $name:ident, $s:expr, (regex = $regex:expr)) => { + let regex = regex::Regex::new($regex).context($crate::framework::macros::attributed_string_type::InvalidRegexSnafu)?; + snafu::ensure!( + regex.is_match($s), + $crate::framework::macros::attributed_string_type::RegexNotMatchedSnafu { + value: $s, + regex: $regex + } + ); + }; + (@from_str $name:ident, $s:expr, is_rfc_1035_label_name) => { + stackable_operator::validation::is_lowercase_rfc_1035_label($s).context($crate::framework::macros::attributed_string_type::InvalidRfc1035LabelNameSnafu)?; + }; + (@from_str $name:ident, $s:expr, is_rfc_1123_dns_subdomain_name) => { + stackable_operator::validation::is_lowercase_rfc_1123_subdomain($s).context($crate::framework::macros::attributed_string_type::InvalidRfc1123DnsSubdomainNameSnafu)?; + }; + (@from_str $name:ident, $s:expr, is_rfc_1123_label_name) => { + stackable_operator::validation::is_lowercase_rfc_1123_label($s).context($crate::framework::macros::attributed_string_type::InvalidRfc1123LabelNameSnafu)?; + }; + (@from_str $name:ident, $s:expr, is_valid_label_value) => { + stackable_operator::kvp::LabelValue::from_str($s).context($crate::framework::macros::attributed_string_type::InvalidLabelValueSnafu)?; + }; + (@from_str $name:ident, $s:expr, is_uid) => { + uuid::Uuid::try_parse($s).context($crate::framework::macros::attributed_string_type::InvalidUidSnafu)?; + }; + + // MIN_LENGTH + + (@min_length) => { + // The minimum String length is 0. + 0 + }; + (@min_length (min_length = $min_length:expr) $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::max( + $min_length, + attributed_string_type!(@min_length $($attribute)*) + ) + }; + (@min_length (max_length = $max_length:expr) $($attribute:tt)*) => { + // max_length has no opinion on the min_length. + attributed_string_type!(@min_length $($attribute)*) + }; + (@min_length (regex = $regex:expr) $($attribute:tt)*) => { + // regex has no influence on the min_length. + attributed_string_type!(@min_length $($attribute)*) + }; + (@min_length is_rfc_1035_label_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::max( + 1, + attributed_string_type!(@min_length $($attribute)*) + ) + }; + (@min_length is_rfc_1123_dns_subdomain_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::max( + 1, + attributed_string_type!(@min_length $($attribute)*) + ) + }; + (@min_length is_rfc_1123_label_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::max( + 1, + attributed_string_type!(@min_length $($attribute)*) + ) + }; + (@min_length is_valid_label_value $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::max( + 1, + attributed_string_type!(@min_length $($attribute)*) + ) + }; + (@min_length is_uid $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::max( + uuid::fmt::Hyphenated::LENGTH, + attributed_string_type!(@min_length $($attribute)*) + ) + }; + + // MAX_LENGTH + + (@max_length) => { + // If there is no other max_length defined, then the upper bound is usize::MAX. + usize::MAX + }; + (@max_length (min_length = $min_length:expr) $($attribute:tt)*) => { + // min_length has no opinion on the max_length. + attributed_string_type!(@max_length $($attribute)*) + }; + (@max_length (max_length = $max_length:expr) $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::min( + $max_length, + attributed_string_type!(@max_length $($attribute)*) + ) + }; + (@max_length (regex = $regex:expr) $($attribute:tt)*) => { + // regex has no influence on the max_length. + attributed_string_type!(@max_length $($attribute)*) + }; + (@max_length is_rfc_1035_label_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::min( + stackable_operator::validation::RFC_1035_LABEL_MAX_LENGTH, + attributed_string_type!(@max_length $($attribute)*) + ) + }; + (@max_length is_rfc_1123_dns_subdomain_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::min( + stackable_operator::validation::RFC_1123_SUBDOMAIN_MAX_LENGTH, + attributed_string_type!(@max_length $($attribute)*) + ) + }; + (@max_length is_rfc_1123_label_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::min( + stackable_operator::validation::RFC_1123_LABEL_MAX_LENGTH, + attributed_string_type!(@max_length $($attribute)*) + ) + }; + (@max_length is_valid_label_value $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::min( + $crate::framework::macros::attributed_string_type::MAX_LABEL_VALUE_LENGTH, + attributed_string_type!(@max_length $($attribute)*) + ) + }; + (@max_length is_uid $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::min( + uuid::fmt::Hyphenated::LENGTH, + attributed_string_type!(@max_length $($attribute)*) + ) + }; + + // REGEX + + (@regex) => { + // Everything is allowed if there is no other regular expression. + $crate::framework::macros::attributed_string_type::Regex::MatchAll + }; + (@regex (min_length = $min_length:expr) $($attribute:tt)*) => { + // min_length has no influence on the regular expression. + attributed_string_type!(@regex $($attribute)*) + }; + (@regex (max_length = $max_length:expr) $($attribute:tt)*) => { + // max_length has no influence on the regular expression. + attributed_string_type!(@regex $($attribute)*) + }; + (@regex (regex = $regex:expr) $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::Regex::Expression($regex) + .combine(attributed_string_type!(@regex $($attribute)*)) + }; + (@regex is_rfc_1035_label_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::Regex::Expression(stackable_operator::validation::LOWERCASE_RFC_1035_LABEL_FMT) + .combine(attributed_string_type!(@regex $($attribute)*)) + }; + (@regex is_rfc_1123_dns_subdomain_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::Regex::Expression(stackable_operator::validation::LOWERCASE_RFC_1123_SUBDOMAIN_FMT) + .combine(attributed_string_type!(@regex $($attribute)*)) + }; + (@regex is_rfc_1123_label_name $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::Regex::Expression(stackable_operator::validation::LOWERCASE_RFC_1123_LABEL_FMT) + .combine(attributed_string_type!(@regex $($attribute)*)) + }; + (@regex is_valid_label_value $($attribute:tt)*) => { + // regular expression from stackable_operator::kvp::label::LABEL_VALUE_REGEX + $crate::framework::macros::attributed_string_type::Regex::Expression("[a-z0-9A-Z]([a-z0-9A-Z-_.]*[a-z0-9A-Z]+)?") + .combine(attributed_string_type!(@regex $($attribute)*)) + }; + (@regex is_uid $($attribute:tt)*) => { + $crate::framework::macros::attributed_string_type::Regex::Expression("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") + .combine(attributed_string_type!(@regex $($attribute)*)) + }; + + // additional constants and trait implementations + + (@trait_impl $name:ident, (min_length = $max_length:expr)) => { + }; + (@trait_impl $name:ident, (max_length = $max_length:expr)) => { + }; + (@trait_impl $name:ident, (regex = $regex:expr)) => { + }; + (@trait_impl $name:ident, is_rfc_1035_label_name) => { + impl $name { + pub const IS_RFC_1035_LABEL_NAME: bool = true; + pub const IS_RFC_1123_LABEL_NAME: bool = true; + pub const IS_RFC_1123_SUBDOMAIN_NAME: bool = true; + } + }; + (@trait_impl $name:ident, is_rfc_1123_dns_subdomain_name) => { + impl $name { + pub const IS_RFC_1123_SUBDOMAIN_NAME: bool = true; + } + }; + (@trait_impl $name:ident, is_rfc_1123_label_name) => { + impl $name { + pub const IS_RFC_1123_LABEL_NAME: bool = true; + pub const IS_RFC_1123_SUBDOMAIN_NAME: bool = true; + } + }; + (@trait_impl $name:ident, is_valid_label_value) => { + impl $name { + pub const IS_VALID_LABEL_VALUE: bool = true; + } + + impl $crate::framework::NameIsValidLabelValue for $name { + fn to_label_value(&self) -> String { + self.0.clone() + } + } + }; + (@trait_impl $name:ident, is_uid) => { + impl From for $name { + fn from(value: uuid::Uuid) -> Self { + Self(value.to_string()) + } + } + + impl From<&uuid::Uuid> for $name { + fn from(value: &uuid::Uuid) -> Self { + Self(value.to_string()) + } + } + }; +} + +/// Returns the minimum of the given values. +/// +/// As opposed to [`std::cmp::min`], this function can be used at compile-time. +/// +/// # Examples +/// +/// ```rust +/// assert_eq!(2, min(2, 3)); +/// assert_eq!(4, min(5, 4)); +/// assert_eq!(1, min(1, 1)); +/// ``` +pub const fn min(x: usize, y: usize) -> usize { + if x < y { x } else { y } +} + +/// Returns the maximum of the given values. +/// +/// As opposed to [`std::cmp::max`], this function can be used at compile-time. +/// +/// # Examples +/// +/// ```rust +/// assert_eq!(3, max(2, 3)); +/// assert_eq!(5, max(5, 4)); +/// assert_eq!(1, max(1, 1)); +/// ``` +pub const fn max(x: usize, y: usize) -> usize { + if x < y { y } else { x } +} + +#[cfg(test)] +// `InvalidRegexTest` intentionally contains an invalid regular expression. +#[allow(clippy::invalid_regex)] +mod tests { + use std::str::FromStr; + + use schemars::{JsonSchema, SchemaGenerator}; + use serde_json::{Number, Value, json}; + use uuid::uuid; + + use super::{ErrorDiscriminants, Regex}; + use crate::framework::NameIsValidLabelValue; + + attributed_string_type! { + MinLengthWithoutConstraintsTest, + "min_length test without constraints", + "" + } + + #[test] + fn test_attributed_string_type_min_length_without_constraints() { + type T = MinLengthWithoutConstraintsTest; + + T::test_example(); + assert_eq!(0, T::MIN_LENGTH); + } + + attributed_string_type! { + MinLengthWithConstraintsTest, + "min_length test with constraints", + "test", + (min_length = 2), // should set the minimum length to 2 + (max_length = 8), // should not affect the minimum length + (regex = ".{4}"), // should not affect the minimum length + is_rfc_1035_label_name, // should be overruled by the greater min_length + is_valid_label_value // should be overruled by the greater min_length + } + + #[test] + fn test_attributed_string_type_min_length_with_constraints() { + type T = MinLengthWithConstraintsTest; + + T::test_example(); + assert_eq!(2, T::MIN_LENGTH); + assert_eq!( + Err(ErrorDiscriminants::MinimumLengthNotMet), + T::from_str("a").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + MaxLengthWithoutConstraintsTest, + "max_length test without constraints", + "" + } + + #[test] + fn test_attributed_string_type_max_length_without_constraints() { + type T = MaxLengthWithoutConstraintsTest; + + T::test_example(); + assert_eq!(usize::MAX, T::MAX_LENGTH); + } + + attributed_string_type! { + MaxLengthWithConstraintsTest, + "max_length test with constraints", + "test", + (min_length = 2), // should not affect the maximum length + (max_length = 8), // should set the maximum length to 8 + (regex = ".{4}"), // should not affect the maximum length + is_rfc_1035_label_name, // should be overruled by the lower max_length + is_valid_label_value // should be overruled by the lower max_length + } + + #[test] + fn test_attributed_string_type_max_length_with_constraints() { + type T = MaxLengthWithConstraintsTest; + + T::test_example(); + assert_eq!(8, T::MAX_LENGTH); + assert_eq!( + Err(ErrorDiscriminants::LengthExceeded), + T::from_str("test-12345").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + RegexWithoutConstraintsTest, + "regex test without constraints", + "" + } + + #[test] + fn test_attributed_string_type_regex_without_constraints() { + type T = RegexWithoutConstraintsTest; + + T::test_example(); + assert_eq!(Regex::MatchAll, T::REGEX); + } + + attributed_string_type! { + RegexWithOneConstraintTest, + "regex test with one constraint", + "test", + (min_length = 2), // should not affect the regular expression + (max_length = 8), // should not affect the regular expression + (regex = "[est]{4}") // should set the regular expression to "[est]{4}" + } + + #[test] + fn test_attributed_string_type_regex_with_one_constraint() { + type T = RegexWithOneConstraintTest; + + T::test_example(); + assert_eq!(Regex::Expression("[est]{4}"), T::REGEX); + assert_eq!( + Err(ErrorDiscriminants::RegexNotMatched), + T::from_str("t-st").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + RegexWithMultipleConstraintsTest, + "regex test with multiple constraints", + "test", + (min_length = 2), // should not affect the regular expression + (max_length = 8), // should not affect the regular expression + (regex = "[est]{4}"), // should not be combinable with is_rfc_1123_dns_subdomain_name + is_rfc_1123_dns_subdomain_name // should not be combinable with regex + } + + #[test] + fn test_attributed_string_type_regex_with_multiple_constraints() { + type T = RegexWithMultipleConstraintsTest; + + T::test_example(); + assert_eq!(Regex::Unknown, T::REGEX); + assert_eq!( + Err(ErrorDiscriminants::RegexNotMatched), + T::from_str("t-st").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + InvalidRegexTest, + "regex test with invalid expression", + "test", + (min_length = 2), // should not affect the regular expression + (max_length = 8), // should not affect the regular expression + (regex = "{") // should throw an error at runtime + } + + #[test] + fn test_attributed_string_type_regex_with_invalid_expression() { + type T = InvalidRegexTest; + + // It is not known yet at compile-time that this expression is invalid. + assert_eq!(Regex::Expression("{"), T::REGEX); + assert_eq!( + Err(ErrorDiscriminants::InvalidRegex), + T::from_str("test").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + DisplayFmtTest, + "Display::fmt test", + "test" + } + + #[test] + fn test_attributed_string_type_display_fmt() { + type T = DisplayFmtTest; + + assert_eq!("test", format!("{}", T::from_str_unsafe("test"))); + } + + attributed_string_type! { + StringFromTest, + "String::from test", + "test" + } + + #[test] + fn test_attributed_string_type_string_from() { + type T = StringFromTest; + + T::test_example(); + assert_eq!("test", String::from(T::from_str_unsafe("test"))); + assert_eq!("test", String::from(&T::from_str_unsafe("test"))); + } + + attributed_string_type! { + DeserializeTest, + "serde::Deserialize test", + "test", + (min_length = 2), + (max_length = 4), + (regex = "[est-]+"), + is_rfc_1035_label_name + } + + #[test] + fn test_attributed_string_type_deserialize() { + type T = DeserializeTest; + + T::test_example(); + assert_eq!( + T::from_str_unsafe("test"), + serde_json::from_value(Value::String("test".to_owned())) + .expect("should be deserializable") + ); + assert_eq!( + Err("minimum length not met".to_owned()), + serde_json::from_value::(Value::String("e".to_owned())) + .map_err(|err| err.to_string()) + ); + assert_eq!( + Err("maximum length exceeded".to_owned()), + serde_json::from_value::(Value::String("testt".to_owned())) + .map_err(|err| err.to_string()) + ); + assert_eq!( + Err("regular expression not matched".to_owned()), + serde_json::from_value::(Value::String("abc".to_owned())) + .map_err(|err| err.to_string()) + ); + assert_eq!( + Err("not a valid label name as defined in RFC 1035".to_owned()), + serde_json::from_value::(Value::String("-tst".to_owned())) + .map_err(|err| err.to_string()) + ); + assert_eq!( + Err("invalid type: null, expected a string".to_owned()), + serde_json::from_value::(Value::Null).map_err(|err| err.to_string()) + ); + assert_eq!( + Err("invalid type: boolean `true`, expected a string".to_owned()), + serde_json::from_value::(Value::Bool(true)).map_err(|err| err.to_string()) + ); + assert_eq!( + Err("invalid type: integer `1`, expected a string".to_owned()), + serde_json::from_value::(Value::Number( + Number::from_i128(1).expect("should be a valid number") + )) + .map_err(|err| err.to_string()) + ); + assert_eq!( + Err("invalid type: sequence, expected a string".to_owned()), + serde_json::from_value::(Value::Array(vec![])).map_err(|err| err.to_string()) + ); + assert_eq!( + Err("invalid type: map, expected a string".to_owned()), + serde_json::from_value::(Value::Object(serde_json::Map::new())) + .map_err(|err| err.to_string()) + ); + } + + attributed_string_type! { + SerializeTest, + "serde::Serialize test", + "test" + } + + #[test] + fn test_attributed_string_type_serialize() { + type T = SerializeTest; + + T::test_example(); + assert_eq!( + "\"test\"".to_owned(), + serde_json::to_string(&T::from_str_unsafe("test")).expect("should be serializable") + ); + } + + attributed_string_type! { + JsonSchemaWithoutConstraintsTest, + "JsonSchema test with constraints", + "test" + } + + #[test] + fn test_attributed_string_type_json_schema_without_constaints() { + type T = JsonSchemaWithoutConstraintsTest; + + T::test_example(); + assert_eq!("JsonSchemaWithoutConstraintsTest", T::schema_name()); + assert_eq!( + json!({ + "type": "string", + "minLength": 0, + "maxLength": None::, + "pattern": None:: + }), + T::json_schema(&mut SchemaGenerator::default()) + ); + } + + attributed_string_type! { + JsonSchemaWithConstraintsTest, + "JsonSchema test with constraints", + "test", + (min_length = 4), + (max_length = 8), + (regex = "[est]+") + } + + #[test] + fn test_attributed_string_type_json_schema_with_constraints() { + type T = JsonSchemaWithConstraintsTest; + + T::test_example(); + assert_eq!("JsonSchemaWithConstraintsTest", T::schema_name()); + assert_eq!( + json!({ + "type": "string", + "minLength": 4, + "maxLength": 8, + "pattern": "^[est]+$" + }), + T::json_schema(&mut SchemaGenerator::default()) + ); + } + + attributed_string_type! { + IsRfc1035LabelNameTest, + "is_rfc_1035_label_name test", + "a-b", + is_rfc_1035_label_name + } + + #[test] + fn test_attributed_string_type_is_rfc_1035_label_name() { + type T = IsRfc1035LabelNameTest; + + let _ = T::IS_RFC_1035_LABEL_NAME; + let _ = T::IS_RFC_1123_LABEL_NAME; + let _ = T::IS_RFC_1123_SUBDOMAIN_NAME; + + T::test_example(); + assert_eq!( + Err(ErrorDiscriminants::InvalidRfc1035LabelName), + T::from_str("A").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + IsRfc1123DnsSubdomainNameTest, + "is_rfc_1123_dns_subdomain_name test", + "a-b.c", + is_rfc_1123_dns_subdomain_name + } + + #[test] + fn test_attributed_string_type_is_rfc_1123_dns_subdomain_name() { + type T = IsRfc1123DnsSubdomainNameTest; + + let _ = T::IS_RFC_1123_SUBDOMAIN_NAME; + + T::test_example(); + assert_eq!( + Err(ErrorDiscriminants::InvalidRfc1123DnsSubdomainName), + T::from_str("A").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + IsRfc1123LabelNameTest, + "is_rfc_1123_label_name test", + "1-a", + is_rfc_1123_label_name + } + + #[test] + fn test_attributed_string_type_is_rfc_1123_label_name() { + type T = IsRfc1123LabelNameTest; + + let _ = T::IS_RFC_1123_LABEL_NAME; + let _ = T::IS_RFC_1123_SUBDOMAIN_NAME; + + T::test_example(); + assert_eq!( + Err(ErrorDiscriminants::InvalidRfc1123LabelName), + T::from_str("A").map_err(ErrorDiscriminants::from) + ); + } + + attributed_string_type! { + IsValidLabelValueTest, + "is_valid_label_value test", + "a-_.1", + is_valid_label_value + } + + #[test] + fn test_attributed_string_type_is_valid_label_value() { + type T = IsValidLabelValueTest; + + let _ = T::IS_VALID_LABEL_VALUE; + + T::test_example(); + assert_eq!( + Err(ErrorDiscriminants::InvalidLabelValue), + T::from_str("invalid label value").map_err(ErrorDiscriminants::from) + ); + assert_eq!( + "label-value", + T::from_str_unsafe("label-value").to_label_value() + ); + } + + attributed_string_type! { + IsUidTest, + "is_uid test", + "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", + is_uid + } + + #[test] + fn test_attributed_string_type_is_uid() { + type T = IsUidTest; + + T::test_example(); + assert_eq!( + Err(ErrorDiscriminants::InvalidUid), + T::from_str("invalid UID").map_err(ErrorDiscriminants::from) + ); + assert_eq!( + "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", + T::from(uuid!("c27b3971-ca72-42c1-80a4-abdfc1db0ddd")).to_string() + ); + assert_eq!( + "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", + T::from(&uuid!("c27b3971-ca72-42c1-80a4-abdfc1db0ddd")).to_string() + ); + } +} diff --git a/rust/operator-binary/src/framework/macros/constant.rs b/rust/operator-binary/src/framework/macros/constant.rs new file mode 100644 index 0000000..ae4e9c6 --- /dev/null +++ b/rust/operator-binary/src/framework/macros/constant.rs @@ -0,0 +1,17 @@ +/// Use [`std::sync::LazyLock`] to define a static "constant" from a string. +/// +/// The string is converted into the given type with [`std::str::FromStr::from_str`]. +/// +/// # Examples +/// +/// ```rust +/// constant!(DATA_VOLUME_NAME: VolumeName = "data"); +/// constant!(pub CONFIG_VOLUME_NAME: VolumeName = "config"); +/// ``` +#[macro_export(local_inner_macros)] +macro_rules! constant { + ($qualifier:vis $name:ident: $type:ident = $value:literal) => { + $qualifier static $name: std::sync::LazyLock<$type> = + std::sync::LazyLock::new(|| $type::from_str($value).expect("should be a valid $name")); + }; +} diff --git a/rust/operator-binary/src/framework/product_logging/framework.rs b/rust/operator-binary/src/framework/product_logging/framework.rs index 1b49109..f0639bd 100644 --- a/rust/operator-binary/src/framework/product_logging/framework.rs +++ b/rust/operator-binary/src/framework/product_logging/framework.rs @@ -18,9 +18,9 @@ use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ constant, framework::{ - ConfigMapKey, ConfigMapName, ContainerName, VolumeName, builder::pod::container::{EnvVarName, EnvVarSet, new_container_builder}, role_group_utils, + types::kubernetes::{ConfigMapKey, ConfigMapName, ContainerName, VolumeName}, }, }; @@ -50,7 +50,9 @@ pub enum Error { GetContainerLogConfiguration { container: String }, #[snafu(display("failed to parse the container name"))] - ParseContainerName { source: crate::framework::Error }, + ParseContainerName { + source: crate::framework::macros::attributed_string_type::Error, + }, } type Result = std::result::Result; @@ -241,9 +243,12 @@ mod tests { validate_logging_configuration_for_container, vector_container, }; use crate::framework::{ - ClusterName, ConfigMapName, ContainerName, RoleGroupName, RoleName, VolumeName, builder::pod::container::{EnvVarName, EnvVarSet}, role_group_utils, + types::{ + kubernetes::{ConfigMapName, ContainerName, VolumeName}, + operator::{ClusterName, RoleGroupName, RoleName}, + }, }; #[test] diff --git a/rust/operator-binary/src/framework/role_group_utils.rs b/rust/operator-binary/src/framework/role_group_utils.rs index f7b7f92..cb21faf 100644 --- a/rust/operator-binary/src/framework/role_group_utils.rs +++ b/rust/operator-binary/src/framework/role_group_utils.rs @@ -1,11 +1,10 @@ use std::str::FromStr; -use stackable_operator::validation::RFC_1035_LABEL_MAX_LENGTH; - -use super::{ - ClusterName, ConfigMapName, ListenerName, RoleGroupName, RoleName, StatefulSetName, min, +use super::types::{ + kubernetes::{ConfigMapName, ListenerName, ServiceName, StatefulSetName}, + operator::{ClusterName, RoleGroupName, RoleName}, }; -use crate::{attributed_string_type, framework::ServiceName}; +use crate::attributed_string_type; attributed_string_type! { QualifiedRoleGroupName, @@ -13,7 +12,7 @@ attributed_string_type! { "opensearch-nodes-default", // Suffixes are added to produce resource names. According compile-time checks ensure that // max_length cannot be set higher. - (max_length = min(52, RFC_1035_LABEL_MAX_LENGTH)), + (max_length = 52), is_rfc_1035_label_name, is_valid_label_value } @@ -116,8 +115,8 @@ impl ResourceNames { mod tests { use super::{ClusterName, RoleGroupName, RoleName}; use crate::framework::{ - ConfigMapName, ListenerName, ServiceName, StatefulSetName, role_group_utils::{QualifiedRoleGroupName, ResourceNames}, + types::kubernetes::{ConfigMapName, ListenerName, ServiceName, StatefulSetName}, }; #[test] diff --git a/rust/operator-binary/src/framework/role_utils.rs b/rust/operator-binary/src/framework/role_utils.rs index cfda143..d6facda 100644 --- a/rust/operator-binary/src/framework/role_utils.rs +++ b/rust/operator-binary/src/framework/role_utils.rs @@ -15,10 +15,12 @@ use stackable_operator::{ }; use super::{ - ProductName, RoleBindingName, ServiceAccountName, ServiceName, builder::pod::container::EnvVarSet, + types::{ + kubernetes::{ClusterRoleName, RoleBindingName, ServiceAccountName, ServiceName}, + operator::{ClusterName, ProductName}, + }, }; -use crate::framework::{ClusterName, ClusterRoleName}; /// Variant of [`stackable_operator::role_utils::GenericProductSpecificCommonConfig`] that /// implements [`Merge`] @@ -248,8 +250,11 @@ mod tests { use super::ResourceNames; use crate::framework::{ - ClusterName, ClusterRoleName, ProductName, RoleBindingName, ServiceAccountName, - ServiceName, role_utils::with_validated_config, + role_utils::with_validated_config, + types::{ + kubernetes::{ClusterRoleName, RoleBindingName, ServiceAccountName, ServiceName}, + operator::{ClusterName, ProductName}, + }, }; #[derive(Debug, Fragment, PartialEq)] diff --git a/rust/operator-binary/src/framework/types.rs b/rust/operator-binary/src/framework/types.rs new file mode 100644 index 0000000..fb75cd3 --- /dev/null +++ b/rust/operator-binary/src/framework/types.rs @@ -0,0 +1,2 @@ +pub mod kubernetes; +pub mod operator; diff --git a/rust/operator-binary/src/framework/types/kubernetes.rs b/rust/operator-binary/src/framework/types/kubernetes.rs new file mode 100644 index 0000000..b172806 --- /dev/null +++ b/rust/operator-binary/src/framework/types/kubernetes.rs @@ -0,0 +1,171 @@ +//! Kubernetes (resource) names +use std::str::FromStr; + +use stackable_operator::validation::{RFC_1123_LABEL_MAX_LENGTH, RFC_1123_SUBDOMAIN_MAX_LENGTH}; + +use crate::attributed_string_type; + +attributed_string_type! { + ConfigMapName, + "The name of a ConfigMap", + "opensearch-nodes-default", + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + ConfigMapKey, + "The key for a ConfigMap", + "log4j2.properties", + (min_length = 1), + // see https://github.com/kubernetes/kubernetes/blob/v1.34.1/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L435-L451 + (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), + (regex = "[-._a-zA-Z0-9]+") +} + +attributed_string_type! { + ContainerName, + "The name of a container in a Pod", + "opensearch", + is_rfc_1123_label_name +} + +attributed_string_type! { + ClusterRoleName, + "The name of a ClusterRole", + "opensearch-clusterrole", + // On the one hand, ClusterRoles must only contain characters that are allowed for DNS + // subdomain names, on the other hand, their length does not seem to be restricted – at least + // on Kind. However, 253 characters are sufficient for the Stackable operators, and to avoid + // problems on other Kubernetes providers, the length is restricted here. + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + ListenerName, + "The name of a Listener", + "opensearch-nodes-default", + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + ListenerClassName, + "The name of a Listener", + "external-stable", + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + NamespaceName, + "The name of a Namespace", + "stackable-operators", + is_rfc_1123_label_name, + is_valid_label_value +} + +attributed_string_type! { + PersistentVolumeClaimName, + "The name of a PersistentVolumeClaim", + "config", + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + RoleBindingName, + "The name of a RoleBinding", + "opensearch-rolebinding", + // On the one hand, RoleBindings must only contain characters that are allowed for DNS + // subdomain names, on the other hand, their length does not seem to be restricted – at least + // on Kind. However, 253 characters are sufficient for the Stackable operators, and to avoid + // problems on other Kubernetes providers, the length is restricted here. + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + SecretKey, + "The key for a Secret", + "accessKey", + (min_length = 1), + // see https://github.com/kubernetes/kubernetes/blob/v1.34.1/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L435-L451 + (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), + (regex = "[-._a-zA-Z0-9]+") +} + +attributed_string_type! { + SecretName, + "The name of a Secret", + "opensearch-security-config", + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + ServiceAccountName, + "The name of a ServiceAccount", + "opensearch-serviceaccount", + is_rfc_1123_dns_subdomain_name +} + +attributed_string_type! { + ServiceName, + "The name of a Service", + "opensearch-nodes-default-headless", + is_rfc_1035_label_name, + is_valid_label_value +} + +attributed_string_type! { + StatefulSetName, + "The name of a StatefulSet", + "opensearch-nodes-default", + (max_length = + // see https://github.com/kubernetes/kubernetes/issues/64023 + RFC_1123_LABEL_MAX_LENGTH + - 1 /* dash */ + - 10 /* digits for the controller-revision-hash label */), + is_rfc_1123_label_name, + is_valid_label_value +} + +attributed_string_type! { + Uid, + "A UID", + "c27b3971-ca72-42c1-80a4-abdfc1db0ddd", + is_uid, + is_valid_label_value +} + +attributed_string_type! { + VolumeName, + "The name of a Volume", + "opensearch-nodes-default", + is_rfc_1123_label_name, + is_valid_label_value +} + +#[cfg(test)] +mod tests { + use super::{ + ClusterRoleName, ConfigMapKey, ConfigMapName, ContainerName, ListenerClassName, + ListenerName, NamespaceName, PersistentVolumeClaimName, RoleBindingName, SecretKey, + SecretName, ServiceAccountName, ServiceName, StatefulSetName, Uid, VolumeName, + }; + + #[test] + fn test_attributed_string_type_examples() { + ConfigMapName::test_example(); + ConfigMapKey::test_example(); + ContainerName::test_example(); + ClusterRoleName::test_example(); + ListenerName::test_example(); + ListenerClassName::test_example(); + NamespaceName::test_example(); + PersistentVolumeClaimName::test_example(); + RoleBindingName::test_example(); + SecretKey::test_example(); + SecretName::test_example(); + ServiceAccountName::test_example(); + ServiceName::test_example(); + StatefulSetName::test_example(); + Uid::test_example(); + VolumeName::test_example(); + } +} diff --git a/rust/operator-binary/src/framework/types/operator.rs b/rust/operator-binary/src/framework/types/operator.rs new file mode 100644 index 0000000..d2020ab --- /dev/null +++ b/rust/operator-binary/src/framework/types/operator.rs @@ -0,0 +1,91 @@ +//! Names for operators + +use std::str::FromStr; + +use crate::attributed_string_type; + +attributed_string_type! { + ProductName, + "The name of a product", + "opensearch", + // A suffix is added to produce a label value. An according compile-time check ensures that + // max_length cannot be set higher. + (max_length = 54), + is_rfc_1123_dns_subdomain_name, + is_valid_label_value +} + +attributed_string_type! { + ProductVersion, + "The version of a product", + "3.1.0", + is_valid_label_value +} + +attributed_string_type! { + ClusterName, + "The name of a cluster/stacklet", + "my-opensearch-cluster", + // Suffixes are added to produce resource names. According compile-time checks ensure that + // max_length cannot be set higher. + (max_length = 24), + is_rfc_1035_label_name, + is_valid_label_value +} + +attributed_string_type! { + ControllerName, + "The name of a controller in an operator", + "opensearchcluster", + is_valid_label_value +} + +attributed_string_type! { + OperatorName, + "The name of an operator", + "opensearch.stackable.tech", + is_valid_label_value +} + +attributed_string_type! { + RoleGroupName, + "The name of a role-group name", + "cluster-manager", + // The role-group name is used to produce resource names. To make sure that all resource names + // are valid, max_length is restricted. Compile-time checks ensure that max_length cannot be + // set higher if not other names like the RoleName are set lower accordingly. + (max_length = 16), + is_rfc_1123_label_name, + is_valid_label_value +} + +attributed_string_type! { + RoleName, + "The name of a role name", + "nodes", + // The role name is used to produce resource names. To make sure that all resource names are + // valid, max_length is restricted. Compile-time checks ensure that max_length cannot be set + // higher if not other names like the RoleGroupName are set lower accordingly. + (max_length = 10), + is_rfc_1123_label_name, + is_valid_label_value +} + +#[cfg(test)] +mod tests { + use super::{ + ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, + RoleName, + }; + + #[test] + fn test_attributed_string_type_examples() { + ProductName::test_example(); + ProductVersion::test_example(); + ClusterName::test_example(); + ControllerName::test_example(); + OperatorName::test_example(); + RoleGroupName::test_example(); + RoleName::test_example(); + } +} diff --git a/rust/operator-binary/src/framework/validation.rs b/rust/operator-binary/src/framework/validation.rs deleted file mode 100644 index b4e3a20..0000000 --- a/rust/operator-binary/src/framework/validation.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::sync::LazyLock; - -use regex::Regex; -use snafu::{Snafu, ensure}; -use stackable_operator::validation::RFC_1123_SUBDOMAIN_MAX_LENGTH; - -/// Format of a key for a ConfigMap or Secret -pub const CONFIG_MAP_KEY_FMT: &str = "[-._a-zA-Z0-9]+"; -const CONFIG_MAP_KEY_ERROR_MSG: &str = - "a valid config key must consist of alphanumeric characters, '-', '_' or '.'"; -static CONFIG_MAP_KEY_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(&format!("^{CONFIG_MAP_KEY_FMT}$")).expect("failed to compile ConfigMap key regex") -}); - -#[derive(Debug, Eq, PartialEq, Snafu)] -pub enum Error { - #[snafu(display("value is empty"))] - Empty { value: String }, - - #[snafu(display("value does not match the regular expression"))] - Regex { - value: String, - regex: &'static str, - message: &'static str, - }, - - #[snafu(display("value exceeds the maximum length"))] - TooLong { value: String, max_length: usize }, -} - -type Result = std::result::Result<(), Error>; - -/// Tests if the given value is a valid key for a ConfigMap or Secret -/// -/// see -pub fn is_config_map_key(value: &str) -> Result { - // When adding this function to stackable_operator, use the private functions like - // validate_all. - - ensure!(!value.is_empty(), EmptySnafu { value }); - - let max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH; - ensure!( - value.len() <= max_length, - TooLongSnafu { - value: value.to_owned(), - max_length - } - ); - - ensure!( - CONFIG_MAP_KEY_REGEX.is_match(value), - RegexSnafu { - value: value.to_owned(), - regex: CONFIG_MAP_KEY_FMT, - message: CONFIG_MAP_KEY_ERROR_MSG - } - ); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::{CONFIG_MAP_KEY_ERROR_MSG, CONFIG_MAP_KEY_FMT, Error, is_config_map_key}; - - #[test] - fn test_is_config_map_key() { - assert_eq!(Ok(()), is_config_map_key("_a-A.1")); - - assert_eq!( - Err(Error::Empty { - value: "".to_owned() - }), - is_config_map_key("") - ); - - assert_eq!(Ok(()), is_config_map_key(&"a".repeat(253))); - assert_eq!( - Err(Error::TooLong { - value: "a".repeat(254), - max_length: 253 - }), - is_config_map_key(&"a".repeat(254)) - ); - - assert_eq!( - Err(Error::Regex { - value: " ".to_string(), - regex: CONFIG_MAP_KEY_FMT, - message: CONFIG_MAP_KEY_ERROR_MSG, - }), - is_config_map_key(" ") - ); - } -} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 2852117..e7df8f2 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -2,7 +2,7 @@ use std::{str::FromStr, sync::Arc}; use clap::Parser as _; use crd::{OpenSearchCluster, OpenSearchClusterVersion, v1alpha1}; -use framework::OperatorName; +use framework::types::operator::OperatorName; use futures::{FutureExt, StreamExt}; use snafu::{ResultExt as _, Snafu}; use stackable_operator::{