diff --git a/CHANGELOG.md b/CHANGELOG.md index d973fadc..2d41efd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed + +- Version CRD structs and enums as v1alpha1 ([#636]). + +[#636]: https://github.com/stackabletech/secret-operator/pull/636 + ## [25.7.0] - 2025-07-23 ## [25.7.0-rc1] - 2025-07-18 diff --git a/Cargo.toml b/Cargo.toml index cb667807..a3386bdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" repository = "https://github.com/stackabletech/secret-operator" [workspace.dependencies] -stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", features = ["time", "telemetry"], tag = "stackable-operator-0.96.0" } +stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", features = ["time", "telemetry", "versioned"], tag = "stackable-operator-0.96.0" } krb5 = { git = "https://github.com/stackabletech/krb5-rs.git", tag = "v0.1.0" } anyhow = "1.0" diff --git a/rust/operator-binary/src/backend/cert_manager.rs b/rust/operator-binary/src/backend/cert_manager.rs index 460477f6..80e38fed 100644 --- a/rust/operator-binary/src/backend/cert_manager.rs +++ b/rust/operator-binary/src/backend/cert_manager.rs @@ -20,7 +20,7 @@ use super::{ scope::SecretScope, }; use crate::{ - crd::{self, CertificateKeyGeneration}, + crd::v1alpha1, external_crd::{self, cert_manager::CertificatePrivateKey}, format::SecretData, utils::Unloggable, @@ -99,7 +99,7 @@ impl SecretBackendError for Error { pub struct CertManager { // Not secret per se, but Client isn't Debug: https://github.com/stackabletech/secret-operator/issues/411 pub client: Unloggable, - pub config: crd::CertManagerBackend, + pub config: v1alpha1::CertManagerBackend, } #[async_trait] @@ -160,7 +160,7 @@ impl SecretBackend for CertManager { kind: Some(self.config.issuer.kind.to_string()), }, private_key: match self.config.key_generation { - CertificateKeyGeneration::Rsa { length } => CertificatePrivateKey { + v1alpha1::CertificateKeyGeneration::Rsa { length } => CertificatePrivateKey { algorithm: "RSA".to_string(), size: length, }, diff --git a/rust/operator-binary/src/backend/dynamic.rs b/rust/operator-binary/src/backend/dynamic.rs index 73bc0a73..56464d0a 100644 --- a/rust/operator-binary/src/backend/dynamic.rs +++ b/rust/operator-binary/src/backend/dynamic.rs @@ -15,10 +15,7 @@ use super::{ pod_info::{PodInfo, SchedulingPodInfo}, tls, }; -use crate::{ - crd::{self, SecretClass}, - utils::Unloggable, -}; +use crate::{crd::v1alpha1, utils::Unloggable}; pub struct DynError(Box); @@ -129,10 +126,10 @@ impl SecretBackendError for FromClassError { pub async fn from_class( client: &stackable_operator::client::Client, - class: SecretClass, + class: v1alpha1::SecretClass, ) -> Result, FromClassError> { Ok(match class.spec.backend { - crd::SecretClassBackend::K8sSearch(crd::K8sSearchBackend { + v1alpha1::SecretClassBackend::K8sSearch(v1alpha1::K8sSearchBackend { search_namespace, trust_store_config_map_name, }) => from(super::K8sSearch { @@ -140,7 +137,7 @@ pub async fn from_class( search_namespace, trust_store_config_map_name, }), - crd::SecretClassBackend::AutoTls(crd::AutoTlsBackend { + v1alpha1::SecretClassBackend::AutoTls(v1alpha1::AutoTlsBackend { ca, additional_trust_roots, max_certificate_lifetime, @@ -153,11 +150,11 @@ pub async fn from_class( ) .await?, ), - crd::SecretClassBackend::CertManager(config) => from(super::CertManager { + v1alpha1::SecretClassBackend::CertManager(config) => from(super::CertManager { client: Unloggable(client.clone()), config, }), - crd::SecretClassBackend::KerberosKeytab(crd::KerberosKeytabBackend { + v1alpha1::SecretClassBackend::KerberosKeytab(v1alpha1::KerberosKeytabBackend { realm_name, kdc, admin, @@ -185,14 +182,14 @@ pub enum FromSelectorError { #[snafu(display("failed to get {class}"))] GetSecretClass { source: stackable_operator::client::Error, - class: ObjectRef, + class: ObjectRef, }, #[snafu(display("failed to initialize backend for {class}"))] FromClass { #[snafu(source(from(FromClassError, Box::new)))] source: Box, - class: ObjectRef, + class: ObjectRef, }, } @@ -220,7 +217,7 @@ pub async fn from_selector( ) -> Result, FromSelectorError> { let class_ref = || ObjectRef::new(&selector.class); let class = client - .get::(&selector.class, &()) + .get::(&selector.class, &()) .await .with_context(|_| from_selector_error::GetSecretClassSnafu { class: class_ref() })?; from_class(client, class) diff --git a/rust/operator-binary/src/backend/k8s_search.rs b/rust/operator-binary/src/backend/k8s_search.rs index 5a7a50bf..8ec03f63 100644 --- a/rust/operator-binary/src/backend/k8s_search.rs +++ b/rust/operator-binary/src/backend/k8s_search.rs @@ -20,7 +20,7 @@ use super::{ pod_info::{PodInfo, SchedulingPodInfo}, scope::SecretScope, }; -use crate::{crd::SearchNamespace, format::SecretData, utils::Unloggable}; +use crate::{crd::v1alpha1, format::SecretData, utils::Unloggable}; const LABEL_CLASS: &str = "secrets.stackable.tech/class"; pub(super) const LABEL_SCOPE_NODE: &str = "secrets.stackable.tech/node"; @@ -89,7 +89,7 @@ impl SecretBackendError for Error { pub struct K8sSearch { // Not secret per se, but isn't Debug: https://github.com/stackabletech/secret-operator/issues/411 pub client: Unloggable, - pub search_namespace: SearchNamespace, + pub search_namespace: v1alpha1::SearchNamespace, pub trust_store_config_map_name: Option, } diff --git a/rust/operator-binary/src/backend/kerberos_keytab.rs b/rust/operator-binary/src/backend/kerberos_keytab.rs index d57bf4d8..0efe4da4 100644 --- a/rust/operator-binary/src/backend/kerberos_keytab.rs +++ b/rust/operator-binary/src/backend/kerberos_keytab.rs @@ -22,10 +22,7 @@ use super::{ scope::SecretScope, }; use crate::{ - crd::{ - ActiveDirectorySamAccountNameRules, InvalidKerberosPrincipal, KerberosKeytabBackendAdmin, - KerberosPrincipal, - }, + crd::{KerberosPrincipal, v1alpha1}, format::{SecretData, WellKnownSecretData, well_known}, utils::Unloggable, }; @@ -62,7 +59,9 @@ pub enum Error { }, #[snafu(display("generated invalid Kerberos principal for pod"))] - PodPrincipal { source: InvalidKerberosPrincipal }, + PodPrincipal { + source: v1alpha1::InvalidKerberosPrincipal, + }, #[snafu(display("failed to read the provisioned keytab"))] ReadProvisionedKeytab { source: std::io::Error }, @@ -106,7 +105,7 @@ impl SecretBackendError for Error { pub struct KerberosProfile { pub realm_name: KerberosRealmName, pub kdc: HostName, - pub admin: KerberosKeytabBackendAdmin, + pub admin: v1alpha1::KerberosKeytabBackendAdmin, } #[derive(Debug)] @@ -169,10 +168,10 @@ impl SecretBackend for KerberosKeytab { } = self; let admin_server_clause = match admin { - KerberosKeytabBackendAdmin::Mit { kadmin_server } => { + v1alpha1::KerberosKeytabBackendAdmin::Mit { kadmin_server } => { format!(" admin_server = {kadmin_server}") } - KerberosKeytabBackendAdmin::ActiveDirectory { .. } => String::new(), + v1alpha1::KerberosKeytabBackendAdmin::ActiveDirectory { .. } => String::new(), }; let tmp = tempdir().context(TempSetupSnafu)?; @@ -254,10 +253,10 @@ cluster.local = {realm_name} }) .collect(), admin_backend: match admin { - KerberosKeytabBackendAdmin::Mit { .. } => { + v1alpha1::KerberosKeytabBackendAdmin::Mit { .. } => { stackable_krb5_provision_keytab::AdminBackend::Mit } - KerberosKeytabBackendAdmin::ActiveDirectory { + v1alpha1::KerberosKeytabBackendAdmin::ActiveDirectory { ldap_server, ldap_tls_ca_secret, password_cache_secret, @@ -271,7 +270,7 @@ cluster.local = {realm_name} user_distinguished_name: user_distinguished_name.clone(), schema_distinguished_name: schema_distinguished_name.clone(), generate_sam_account_name: generate_sam_account_name.clone().map( - |ActiveDirectorySamAccountNameRules { + |v1alpha1::ActiveDirectorySamAccountNameRules { prefix, total_length, }| { diff --git a/rust/operator-binary/src/backend/tls/ca.rs b/rust/operator-binary/src/backend/tls/ca.rs index e66b5612..1ee1d0d2 100644 --- a/rust/operator-binary/src/backend/tls/ca.rs +++ b/rust/operator-binary/src/backend/tls/ca.rs @@ -38,7 +38,7 @@ use tracing::{info, info_span, warn}; use crate::{ backend::SecretBackendError, - crd::{AdditionalTrustRoot, CertificateKeyGeneration}, + crd::v1alpha1, utils::{Asn1TimeParseError, Unloggable, asn1time_to_offsetdatetime}, }; @@ -202,7 +202,7 @@ pub struct Config { pub rotate_if_ca_expires_before: Option, /// Configuration how TLS private keys should be created. - pub key_generation: CertificateKeyGeneration, + pub key_generation: v1alpha1::CertificateKeyGeneration, } /// A single certificate authority certificate. @@ -241,7 +241,7 @@ impl CertificateAuthority { Conf::new(ConfMethod::default()).expect("failed to initialize OpenSSL configuration"); let private_key_length = match config.key_generation { - CertificateKeyGeneration::Rsa { length } => length, + v1alpha1::CertificateKeyGeneration::Rsa { length } => length, }; let private_key = Rsa::generate(private_key_length) @@ -348,7 +348,7 @@ impl Manager { pub async fn load_or_create( client: &stackable_operator::client::Client, secret_ref: &SecretReference, - additional_trust_roots: &[AdditionalTrustRoot], + additional_trust_roots: &[v1alpha1::AdditionalTrustRoot], config: &Config, ) -> Result { // Use entry API rather than apply so that we crash and retry on conflicts (to avoid creating spurious certs that we throw away immediately) @@ -496,10 +496,10 @@ impl Manager { let mut additional_trusted_certificates = vec![]; for entry in additional_trust_roots { let certs = match entry { - AdditionalTrustRoot::ConfigMap(config_map) => { + v1alpha1::AdditionalTrustRoot::ConfigMap(config_map) => { Self::read_extra_trust_roots_from_config_map(client, config_map).await? } - AdditionalTrustRoot::Secret(secret) => { + v1alpha1::AdditionalTrustRoot::Secret(secret) => { Self::read_extra_trust_roots_from_secret(client, secret).await? } }; diff --git a/rust/operator-binary/src/backend/tls/mod.rs b/rust/operator-binary/src/backend/tls/mod.rs index a298dd1b..3713935c 100644 --- a/rust/operator-binary/src/backend/tls/mod.rs +++ b/rust/operator-binary/src/backend/tls/mod.rs @@ -33,7 +33,7 @@ use super::{ scope::SecretScope, }; use crate::{ - crd::{self, AdditionalTrustRoot, CertificateKeyGeneration}, + crd::v1alpha1, format::{SecretData, WellKnownSecretData, well_known}, utils::iterator_try_concat_bytes, }; @@ -150,7 +150,7 @@ impl SecretBackendError for Error { pub struct TlsGenerate { ca_manager: ca::Manager, max_cert_lifetime: Duration, - key_generation: CertificateKeyGeneration, + key_generation: v1alpha1::CertificateKeyGeneration, } impl TlsGenerate { @@ -162,13 +162,13 @@ impl TlsGenerate { /// an independent self-signed CA. pub async fn get_or_create_k8s_certificate( client: &stackable_operator::client::Client, - crd::AutoTlsCa { + v1alpha1::AutoTlsCa { secret: ca_secret, auto_generate: auto_generate_ca, ca_certificate_lifetime, key_generation, - }: &crd::AutoTlsCa, - additional_trust_roots: &[AdditionalTrustRoot], + }: &v1alpha1::AutoTlsCa, + additional_trust_roots: &[v1alpha1::AdditionalTrustRoot], max_cert_lifetime: Duration, ) -> Result { Ok(Self { @@ -260,7 +260,7 @@ impl SecretBackend for TlsGenerate { Conf::new(ConfMethod::default()).expect("failed to initialize OpenSSL configuration"); let pod_key_length = match self.key_generation { - CertificateKeyGeneration::Rsa { length } => length, + v1alpha1::CertificateKeyGeneration::Rsa { length } => length, }; let pod_key = Rsa::generate(pod_key_length) diff --git a/rust/operator-binary/src/crd.rs b/rust/operator-binary/src/crd.rs deleted file mode 100644 index d5cb1db7..00000000 --- a/rust/operator-binary/src/crd.rs +++ /dev/null @@ -1,629 +0,0 @@ -use std::{fmt::Display, ops::Deref}; - -use serde::{Deserialize, Serialize}; -use snafu::Snafu; -use stackable_operator::{ - commons::networking::{HostName, KerberosRealmName}, - k8s_openapi::api::core::v1::{ConfigMap, Secret}, - kube::{CustomResource, api::PartialObjectMeta}, - schemars::{self, JsonSchema, schema::Schema}, - shared::time::Duration, -}; -use stackable_secret_operator_crd_utils::{ConfigMapReference, SecretReference}; - -use crate::{backend, format::SecretFormat}; - -/// A [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) is a cluster-global Kubernetes resource -/// that defines a category of secrets that the Secret Operator knows how to provision. -#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[kube( - group = "secrets.stackable.tech", - version = "v1alpha1", - kind = "SecretClass", - crates( - kube_core = "stackable_operator::kube::core", - k8s_openapi = "stackable_operator::k8s_openapi", - schemars = "stackable_operator::schemars" - ) -)] -#[serde(rename_all = "camelCase")] -pub struct SecretClassSpec { - /// Each SecretClass is associated with a single - /// [backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend), - /// which dictates the mechanism for issuing that kind of Secret. - pub backend: SecretClassBackend, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -#[allow(clippy::large_enum_variant)] -pub enum SecretClassBackend { - /// The [`k8sSearch` backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-k8ssearch) - /// can be used to mount Secrets across namespaces into Pods. - K8sSearch(K8sSearchBackend), - - /// The [`autoTls` backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-autotls) - /// issues a TLS certificate signed by the Secret Operator. - /// The certificate authority can be provided by the administrator, or managed automatically by the Secret Operator. - /// - /// A new certificate and key pair will be generated and signed for each Pod, keys or certificates are never reused. - AutoTls(AutoTlsBackend), - - /// The [`experimentalCertManager` backend][1] injects a TLS certificate issued - /// by [cert-manager](https://cert-manager.io/). - /// - /// A new certificate will be requested the first time it is used by a Pod, it - /// will be reused after that (subject to cert-manager renewal rules). - /// - /// [1]: DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-certmanager - #[serde(rename = "experimentalCertManager")] - CertManager(CertManagerBackend), - - /// The [`kerberosKeytab` backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-kerberoskeytab) - /// creates a Kerberos keytab file for a selected realm. - /// The Kerberos KDC and administrator credentials must be provided by the administrator. - KerberosKeytab(KerberosKeytabBackend), -} - -impl SecretClassBackend { - // Currently no `refers_to_*` method actually returns more than one element, - // but returning `Iterator` instead of `Option` to ensure that all consumers are ready - // for adding more conditions. - - // The matcher methods are on the CRD type rather than the initialized `Backend` impls - // to avoid having to initialize the backend for each watch event. - - /// Returns the conditions where the backend refers to `config_map`. - pub fn refers_to_config_map( - &self, - config_map: &PartialObjectMeta, - ) -> impl Iterator { - let cm_namespace = config_map.metadata.namespace.as_deref(); - match self { - Self::K8sSearch(backend) => { - let name_matches = backend.trust_store_config_map_name == config_map.metadata.name; - cm_namespace - .filter(|_| name_matches) - .and_then(|cm_ns| backend.search_namespace.matches_namespace(cm_ns)) - } - Self::AutoTls(_) => None, - Self::CertManager(_) => None, - Self::KerberosKeytab(_) => None, - } - .into_iter() - } - - /// Returns the conditions where the backend refers to `secret`. - pub fn refers_to_secret( - &self, - secret: &PartialObjectMeta, - ) -> impl Iterator { - match self { - Self::AutoTls(backend) => { - (backend.ca.secret == *secret).then_some(SearchNamespaceMatchCondition::True) - } - Self::K8sSearch(_) => None, - Self::CertManager(_) => None, - Self::KerberosKeytab(_) => None, - } - .into_iter() - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct K8sSearchBackend { - /// Configures the namespace searched for Secret objects. - pub search_namespace: SearchNamespace, - - /// Name of a ConfigMap that contains the information required to validate against this SecretClass. - /// - /// Resolved relative to `search_namespace`. - /// - /// Required to request a TrustStore for this SecretClass. - pub trust_store_config_map_name: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum SearchNamespace { - /// The Secret objects are located in the same namespace as the Pod object. - /// Should be used for Secrets that are provisioned by the application administrator. - Pod {}, - - /// The Secret objects are located in a single global namespace. - /// Should be used for secrets that are provisioned by the cluster administrator. - Name(String), -} - -impl SearchNamespace { - pub fn resolve<'a>(&'a self, pod_namespace: &'a str) -> &'a str { - match self { - SearchNamespace::Pod {} => pod_namespace, - SearchNamespace::Name(ns) => ns, - } - } - - /// Returns [`Some`] if this `SearchNamespace` could possibly match an object in the namespace - /// `object_namespace`, otherwise [`None`]. - /// - /// This is optimistic, you then need to call [`SearchNamespaceMatchCondition::matches_pod_namespace`] - /// to evaluate the match for a specific pod's namespace. - pub fn matches_namespace( - &self, - object_namespace: &str, - ) -> Option { - match self { - SearchNamespace::Pod {} => Some(SearchNamespaceMatchCondition::IfPodIsInNamespace { - namespace: object_namespace.to_string(), - }), - SearchNamespace::Name(ns) => { - (ns == object_namespace).then_some(SearchNamespaceMatchCondition::True) - } - } - } -} - -/// A partially evaluated match returned by [`SearchNamespace::matches_namespace`]. -/// Use [`Self::matches_pod_namespace`] to evaluate fully. -#[derive(Debug)] -pub enum SearchNamespaceMatchCondition { - /// The target object matches the search namespace. - True, - - /// The target object only matches the search namespace if mounted into a pod in - /// `namespace`. - IfPodIsInNamespace { namespace: String }, -} - -impl SearchNamespaceMatchCondition { - pub fn matches_pod_namespace(&self, pod_ns: &str) -> bool { - match self { - Self::True => true, - Self::IfPodIsInNamespace { namespace } => namespace == pod_ns, - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct AutoTlsBackend { - /// Configures the certificate authority used to issue Pod certificates. - pub ca: AutoTlsCa, - - /// Additional trust roots which are added to the provided `ca.crt` file. - #[serde(default)] - pub additional_trust_roots: Vec, - - /// Maximum lifetime the created certificates are allowed to have. - /// In case consumers request a longer lifetime than allowed by this setting, - /// the lifetime will be the minimum of both, so this setting takes precedence. - /// The default value is 15 days. - #[serde(default = "AutoTlsBackend::default_max_certificate_lifetime")] - pub max_certificate_lifetime: Duration, -} - -impl AutoTlsBackend { - fn default_max_certificate_lifetime() -> Duration { - backend::tls::DEFAULT_MAX_CERT_LIFETIME - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct AutoTlsCa { - /// Reference (name and namespace) to a Kubernetes Secret object where the CA certificate - /// and key is stored in the keys `ca.crt` and `ca.key` respectively. - pub secret: SecretReference, - - /// Whether the certificate authority should be managed by Secret Operator, including being generated - /// if it does not already exist. - // TODO: Consider renaming to `manage` for v1alpha2 - #[serde(default)] - pub auto_generate: bool, - - /// The lifetime of each generated certificate authority. - /// - /// Should always be more than double `maxCertificateLifetime`. - /// - /// If `autoGenerate: true` then the Secret Operator will prepare a new CA certificate the old CA approaches expiration. - /// If `autoGenerate: false` then the Secret Operator will log a warning instead. - #[serde(default = "AutoTlsCa::default_ca_certificate_lifetime")] - pub ca_certificate_lifetime: Duration, - - /// The algorithm used to generate a key pair and required configuration settings. - /// Currently only RSA and a key length of 2048, 3072 or 4096 bits can be configured. - #[serde(default)] - pub key_generation: CertificateKeyGeneration, -} - -impl AutoTlsCa { - fn default_ca_certificate_lifetime() -> Duration { - backend::tls::DEFAULT_CA_CERT_LIFETIME - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum AdditionalTrustRoot { - /// Reference (name and namespace) to a Kubernetes ConfigMap object where additional - /// certificates are stored. - /// The extensions of the keys denote its contents: A key suffixed with `.crt` contains a stack - /// of base64 encoded DER certificates, a key suffixed with `.der` contains a binary DER - /// certificate. - ConfigMap(ConfigMapReference), - - /// Reference (name and namespace) to a Kubernetes Secret object where additional certificates - /// are stored. - /// The extensions of the keys denote its contents: A key suffixed with `.crt` contains a stack - /// of base64 encoded DER certificates, a key suffixed with `.der` contains a binary DER - /// certificate. - Secret(SecretReference), -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum CertificateKeyGeneration { - Rsa { - /// The amount of bits used for generating the RSA keypair. - /// Currently, `2048`, `3072` and `4096` are supported. Defaults to `2048` bits. - #[schemars(schema_with = "CertificateKeyGeneration::tls_key_length_schema")] - length: u32, - }, -} - -impl CertificateKeyGeneration { - pub const RSA_KEY_LENGTH_2048: u32 = 2048; - pub const RSA_KEY_LENGTH_3072: u32 = 3072; - pub const RSA_KEY_LENGTH_4096: u32 = 4096; - - // Could not get a "standard" enum with assigned values/discriminants to work as integers in the schema - // The following was generated and requires the length to be provided as string (we want an integer) - // keyGeneration: - // default: - // rsa: - // length: '2048' - // oneOf: - // - required: - // - rsa - // properties: - // rsa: - // properties: - // length: - // enum: - // - '2048' - // - '3072' - // - '4096' - // type: string - pub fn tls_key_length_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { - serde_json::from_value(serde_json::json!({ - "type": "integer", - "enum": [ - Self::RSA_KEY_LENGTH_2048, - Self::RSA_KEY_LENGTH_3072, - Self::RSA_KEY_LENGTH_4096 - ] - })) - .expect("Failed to parse JSON of custom tls key length schema") - } -} - -impl Default for CertificateKeyGeneration { - fn default() -> Self { - Self::Rsa { - length: Self::RSA_KEY_LENGTH_2048, - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct CertManagerBackend { - /// A reference to the cert-manager issuer that the certificates should be requested from. - pub issuer: CertManagerIssuer, - - /// The default lifetime of certificates. - /// - /// Defaults to 1 day. This may need to be increased for external issuers that impose rate limits (such as Let's Encrypt). - #[serde(default = "CertManagerBackend::default_certificate_lifetime")] - pub default_certificate_lifetime: Duration, - - /// The algorithm used to generate a key pair and required configuration settings. - /// Currently only RSA and a key length of 2048, 3072 or 4096 bits can be configured. - #[serde(default)] - pub key_generation: CertificateKeyGeneration, -} - -impl CertManagerBackend { - fn default_certificate_lifetime() -> Duration { - backend::cert_manager::DEFAULT_CERT_LIFETIME - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct CertManagerIssuer { - /// The kind of the issuer, Issuer or ClusterIssuer. - /// - /// If Issuer then it must be in the same namespace as the Pods using it. - pub kind: CertManagerIssuerKind, - - /// The name of the issuer. - pub name: String, -} - -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, JsonSchema, strum::Display)] -pub enum CertManagerIssuerKind { - /// An [Issuer](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.Issuer) in the same namespace as the Pod. - Issuer, - - /// A cluster-scoped [ClusterIssuer](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.ClusterIssuer). - ClusterIssuer, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct KerberosKeytabBackend { - /// The name of the Kerberos realm. This should be provided by the Kerberos administrator. - pub realm_name: KerberosRealmName, - - /// The hostname of the Kerberos Key Distribution Center (KDC). - /// This should be provided by the Kerberos administrator. - pub kdc: HostName, - - /// Kerberos admin configuration settings. - pub admin: KerberosKeytabBackendAdmin, - - /// Reference (`name` and `namespace`) to a K8s Secret object where a - /// keytab with administrative privileges is stored in the key `keytab`. - pub admin_keytab_secret: SecretReference, - - /// The admin principal. - pub admin_principal: KerberosPrincipal, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum KerberosKeytabBackendAdmin { - /// Credentials should be provisioned in a MIT Kerberos Admin Server. - #[serde(rename_all = "camelCase")] - Mit { - /// The hostname of the Kerberos Admin Server. - /// This should be provided by the Kerberos administrator. - kadmin_server: HostName, - }, - - /// Credentials should be provisioned in a Microsoft Active Directory domain. - #[serde(rename_all = "camelCase")] - ActiveDirectory { - /// An AD LDAP server, such as the AD Domain Controller. - /// This must match the server’s FQDN, or GSSAPI authentication will fail. - ldap_server: HostName, - - /// Reference (name and namespace) to a Kubernetes Secret object containing - /// the TLS CA (in `ca.crt`) that the LDAP server’s certificate should be authenticated against. - ldap_tls_ca_secret: SecretReference, - - /// Reference (name and namespace) to a Kubernetes Secret object where workload - /// passwords will be stored. This must not be accessible to end users. - password_cache_secret: SecretReference, - - /// The root Distinguished Name (DN) where service accounts should be provisioned, - /// typically `CN=Users,{domain_dn}`. - user_distinguished_name: String, - - /// The root Distinguished Name (DN) for AD-managed schemas, - /// typically `CN=Schema,CN=Configuration,{domain_dn}`. - schema_distinguished_name: String, - - /// Allows samAccountName generation for new accounts to be customized. - /// Note that setting this field (even if empty) makes the Secret Operator take - /// over the generation duty from the domain controller. - #[serde(rename = "experimentalGenerateSamAccountName")] - generate_sam_account_name: Option, - }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct ActiveDirectorySamAccountNameRules { - /// A prefix to be prepended to generated samAccountNames. - #[serde(default)] - pub prefix: String, - /// The total length of generated samAccountNames, _including_ `prefix`. - /// Must be larger than the length of `prefix`, but at most `20`. - /// - /// Note that this should be as large as possible, to minimize the risk of collisions. - #[serde(default = "ActiveDirectorySamAccountNameRules::default_total_length")] - pub total_length: u8, -} - -impl ActiveDirectorySamAccountNameRules { - fn default_total_length() -> u8 { - // Default AD samAccountName length limit - 20 - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[serde(try_from = "String", into = "String")] -pub struct KerberosPrincipal(String); -#[derive(Debug, Snafu)] -#[snafu(module)] -pub enum InvalidKerberosPrincipal { - #[snafu(display( - "principal contains illegal characters (allowed: alphanumeric, /, @, -, _, and .)" - ))] - IllegalCharacter, - - #[snafu(display("principal may not start with a dash"))] - StartWithDash, -} -impl TryFrom for KerberosPrincipal { - type Error = InvalidKerberosPrincipal; - - fn try_from(value: String) -> Result { - if value.starts_with('-') { - invalid_kerberos_principal::StartWithDashSnafu.fail() - } else if value.contains(|chr: char| { - !chr.is_alphanumeric() - && chr != '/' - && chr != '@' - && chr != '.' - && chr != '-' - && chr != '_' - }) { - invalid_kerberos_principal::IllegalCharacterSnafu.fail() - } else { - Ok(KerberosPrincipal(value)) - } - } -} -impl From for String { - fn from(value: KerberosPrincipal) -> Self { - value.0 - } -} -impl Display for KerberosPrincipal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.0) - } -} -impl Deref for KerberosPrincipal { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// A [TrustStore](DOCS_BASE_URL_PLACEHOLDER/secret-operator/truststore) requests information about how to -/// validate secrets issued by a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass). -/// -/// The requested information is written to a ConfigMap with the same name as the TrustStore. -#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[kube( - group = "secrets.stackable.tech", - version = "v1alpha1", - kind = "TrustStore", - namespaced, - crates( - kube_core = "stackable_operator::kube::core", - k8s_openapi = "stackable_operator::k8s_openapi", - schemars = "stackable_operator::schemars" - ) -)] -#[serde(rename_all = "camelCase")] -pub struct TrustStoreSpec { - /// The name of the SecretClass that the request concerns. - pub secret_class_name: String, - - /// The [format](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#format) that the data should be converted into. - pub format: Option, -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{ - backend::tls::{DEFAULT_CA_CERT_LIFETIME, DEFAULT_MAX_CERT_LIFETIME}, - crd::{AutoTlsBackend, SecretClass, SecretClassSpec}, - }; - - #[test] - fn test_deserialization() { - let input: &str = r#" - apiVersion: secrets.stackable.tech/v1alpha1 - kind: SecretClass - metadata: - name: tls - spec: - backend: - autoTls: - ca: - secret: - name: secret-provisioner-tls-ca - namespace: default - keyGeneration: - rsa: - length: 3072 - "#; - let deserializer = serde_yaml::Deserializer::from_str(input); - let secret_class: SecretClass = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); - assert_eq!( - secret_class.spec, - SecretClassSpec { - backend: crate::crd::SecretClassBackend::AutoTls(AutoTlsBackend { - ca: crate::crd::AutoTlsCa { - secret: SecretReference { - name: "secret-provisioner-tls-ca".to_string(), - namespace: "default".to_string(), - }, - auto_generate: false, - ca_certificate_lifetime: DEFAULT_CA_CERT_LIFETIME, - key_generation: CertificateKeyGeneration::Rsa { - length: CertificateKeyGeneration::RSA_KEY_LENGTH_3072 - } - }, - additional_trust_roots: vec![], - max_certificate_lifetime: DEFAULT_MAX_CERT_LIFETIME, - }) - } - ); - - let input: &str = r#" - apiVersion: secrets.stackable.tech/v1alpha1 - kind: SecretClass - metadata: - name: tls - spec: - backend: - autoTls: - ca: - secret: - name: secret-provisioner-tls-ca - namespace: default - autoGenerate: true - caCertificateLifetime: 100d - additionalTrustRoots: - - configMap: - name: tls-root-ca-config-map - namespace: default - - secret: - name: tls-root-ca-secret - namespace: default - maxCertificateLifetime: 31d - "#; - let deserializer = serde_yaml::Deserializer::from_str(input); - let secret_class: SecretClass = - serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); - assert_eq!( - secret_class.spec, - SecretClassSpec { - backend: crate::crd::SecretClassBackend::AutoTls(AutoTlsBackend { - ca: crate::crd::AutoTlsCa { - secret: SecretReference { - name: "secret-provisioner-tls-ca".to_string(), - namespace: "default".to_string(), - }, - auto_generate: true, - ca_certificate_lifetime: Duration::from_days_unchecked(100), - key_generation: CertificateKeyGeneration::default() - }, - additional_trust_roots: vec![ - AdditionalTrustRoot::ConfigMap(ConfigMapReference { - name: "tls-root-ca-config-map".to_string(), - namespace: "default".to_string(), - }), - AdditionalTrustRoot::Secret(SecretReference { - name: "tls-root-ca-secret".to_string(), - namespace: "default".to_string(), - }) - ], - max_certificate_lifetime: Duration::from_days_unchecked(31), - }) - } - ); - } -} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs new file mode 100644 index 00000000..db49af6f --- /dev/null +++ b/rust/operator-binary/src/crd/mod.rs @@ -0,0 +1,429 @@ +use serde::{Deserialize, Serialize}; +use stackable_operator::{ + commons::networking::{HostName, KerberosRealmName}, + kube::CustomResource, + schemars::{self, JsonSchema}, + shared::time::Duration, + versioned::versioned, +}; +use stackable_secret_operator_crd_utils::{ConfigMapReference, SecretReference}; + +use crate::format::SecretFormat; + +mod v1alpha1_impl; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(try_from = "String", into = "String")] +pub struct KerberosPrincipal(String); + +#[versioned( + version(name = "v1alpha1"), + crates( + kube_core = "stackable_operator::kube::core", + kube_client = "stackable_operator::kube::client", + k8s_openapi = "stackable_operator::k8s_openapi", + schemars = "stackable_operator::schemars", + versioned = "stackable_operator::versioned" + ) +)] +pub mod versioned { + pub mod v1alpha1 { + pub use v1alpha1_impl::*; + } + + /// A [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) is a cluster-global Kubernetes resource + /// that defines a category of secrets that the Secret Operator knows how to provision. + #[versioned(crd(group = "secrets.stackable.tech"))] + #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct SecretClassSpec { + /// Each SecretClass is associated with a single + /// [backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend), + /// which dictates the mechanism for issuing that kind of Secret. + pub backend: SecretClassBackend, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + #[allow(clippy::large_enum_variant)] + pub enum SecretClassBackend { + /// The [`k8sSearch` backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-k8ssearch) + /// can be used to mount Secrets across namespaces into Pods. + K8sSearch(K8sSearchBackend), + + /// The [`autoTls` backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-autotls) + /// issues a TLS certificate signed by the Secret Operator. + /// The certificate authority can be provided by the administrator, or managed automatically by the Secret Operator. + /// + /// A new certificate and key pair will be generated and signed for each Pod, keys or certificates are never reused. + AutoTls(AutoTlsBackend), + + /// The [`experimentalCertManager` backend][1] injects a TLS certificate issued + /// by [cert-manager](https://cert-manager.io/). + /// + /// A new certificate will be requested the first time it is used by a Pod, it + /// will be reused after that (subject to cert-manager renewal rules). + /// + /// [1]: DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-certmanager + #[serde(rename = "experimentalCertManager")] + CertManager(CertManagerBackend), + + /// The [`kerberosKeytab` backend](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#backend-kerberoskeytab) + /// creates a Kerberos keytab file for a selected realm. + /// The Kerberos KDC and administrator credentials must be provided by the administrator. + KerberosKeytab(KerberosKeytabBackend), + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct K8sSearchBackend { + /// Configures the namespace searched for Secret objects. + pub search_namespace: SearchNamespace, + + /// Name of a ConfigMap that contains the information required to validate against this SecretClass. + /// + /// Resolved relative to `search_namespace`. + /// + /// Required to request a TrustStore for this SecretClass. + pub trust_store_config_map_name: Option, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub enum SearchNamespace { + /// The Secret objects are located in the same namespace as the Pod object. + /// Should be used for Secrets that are provisioned by the application administrator. + Pod {}, + + /// The Secret objects are located in a single global namespace. + /// Should be used for secrets that are provisioned by the cluster administrator. + Name(String), + } + + /// A partially evaluated match returned by [`SearchNamespace::matches_namespace`]. + /// Use [`Self::matches_pod_namespace`] to evaluate fully. + #[derive(Debug)] + pub enum SearchNamespaceMatchCondition { + /// The target object matches the search namespace. + True, + + /// The target object only matches the search namespace if mounted into a pod in + /// `namespace`. + IfPodIsInNamespace { namespace: String }, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct AutoTlsBackend { + /// Configures the certificate authority used to issue Pod certificates. + pub ca: AutoTlsCa, + + /// Additional trust roots which are added to the provided `ca.crt` file. + #[serde(default)] + pub additional_trust_roots: Vec, + + /// Maximum lifetime the created certificates are allowed to have. + /// In case consumers request a longer lifetime than allowed by this setting, + /// the lifetime will be the minimum of both, so this setting takes precedence. + /// The default value is 15 days. + #[serde(default = "AutoTlsBackend::default_max_certificate_lifetime")] + pub max_certificate_lifetime: Duration, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct AutoTlsCa { + /// Reference (name and namespace) to a Kubernetes Secret object where the CA certificate + /// and key is stored in the keys `ca.crt` and `ca.key` respectively. + pub secret: SecretReference, + + /// Whether the certificate authority should be managed by Secret Operator, including being generated + /// if it does not already exist. + // TODO: Consider renaming to `manage` for v1alpha2 + #[serde(default)] + pub auto_generate: bool, + + /// The lifetime of each generated certificate authority. + /// + /// Should always be more than double `maxCertificateLifetime`. + /// + /// If `autoGenerate: true` then the Secret Operator will prepare a new CA certificate the old CA approaches expiration. + /// If `autoGenerate: false` then the Secret Operator will log a warning instead. + #[serde(default = "AutoTlsCa::default_ca_certificate_lifetime")] + pub ca_certificate_lifetime: Duration, + + /// The algorithm used to generate a key pair and required configuration settings. + /// Currently only RSA and a key length of 2048, 3072 or 4096 bits can be configured. + #[serde(default)] + pub key_generation: CertificateKeyGeneration, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub enum AdditionalTrustRoot { + /// Reference (name and namespace) to a Kubernetes ConfigMap object where additional + /// certificates are stored. + /// The extensions of the keys denote its contents: A key suffixed with `.crt` contains a stack + /// of base64 encoded DER certificates, a key suffixed with `.der` contains a binary DER + /// certificate. + ConfigMap(ConfigMapReference), + + /// Reference (name and namespace) to a Kubernetes Secret object where additional certificates + /// are stored. + /// The extensions of the keys denote its contents: A key suffixed with `.crt` contains a stack + /// of base64 encoded DER certificates, a key suffixed with `.der` contains a binary DER + /// certificate. + Secret(SecretReference), + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub enum CertificateKeyGeneration { + Rsa { + /// The amount of bits used for generating the RSA keypair. + /// Currently, `2048`, `3072` and `4096` are supported. Defaults to `2048` bits. + #[schemars(schema_with = "CertificateKeyGeneration::tls_key_length_schema")] + length: u32, + }, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct CertManagerBackend { + /// A reference to the cert-manager issuer that the certificates should be requested from. + pub issuer: CertManagerIssuer, + + /// The default lifetime of certificates. + /// + /// Defaults to 1 day. This may need to be increased for external issuers that impose rate limits (such as Let's Encrypt). + #[serde(default = "CertManagerBackend::default_certificate_lifetime")] + pub default_certificate_lifetime: Duration, + + /// The algorithm used to generate a key pair and required configuration settings. + /// Currently only RSA and a key length of 2048, 3072 or 4096 bits can be configured. + #[serde(default)] + pub key_generation: CertificateKeyGeneration, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct CertManagerIssuer { + /// The kind of the issuer, Issuer or ClusterIssuer. + /// + /// If Issuer then it must be in the same namespace as the Pods using it. + pub kind: CertManagerIssuerKind, + + /// The name of the issuer. + pub name: String, + } + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, JsonSchema, strum::Display)] + pub enum CertManagerIssuerKind { + /// An [Issuer](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.Issuer) in the same namespace as the Pod. + Issuer, + + /// A cluster-scoped [ClusterIssuer](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.ClusterIssuer). + ClusterIssuer, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct KerberosKeytabBackend { + /// The name of the Kerberos realm. This should be provided by the Kerberos administrator. + pub realm_name: KerberosRealmName, + + /// The hostname of the Kerberos Key Distribution Center (KDC). + /// This should be provided by the Kerberos administrator. + pub kdc: HostName, + + /// Kerberos admin configuration settings. + pub admin: KerberosKeytabBackendAdmin, + + /// Reference (`name` and `namespace`) to a K8s Secret object where a + /// keytab with administrative privileges is stored in the key `keytab`. + pub admin_keytab_secret: SecretReference, + + /// The admin principal. + pub admin_principal: KerberosPrincipal, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub enum KerberosKeytabBackendAdmin { + /// Credentials should be provisioned in a MIT Kerberos Admin Server. + #[serde(rename_all = "camelCase")] + Mit { + /// The hostname of the Kerberos Admin Server. + /// This should be provided by the Kerberos administrator. + kadmin_server: HostName, + }, + + /// Credentials should be provisioned in a Microsoft Active Directory domain. + #[serde(rename_all = "camelCase")] + ActiveDirectory { + /// An AD LDAP server, such as the AD Domain Controller. + /// This must match the server’s FQDN, or GSSAPI authentication will fail. + ldap_server: HostName, + + /// Reference (name and namespace) to a Kubernetes Secret object containing + /// the TLS CA (in `ca.crt`) that the LDAP server’s certificate should be authenticated against. + ldap_tls_ca_secret: SecretReference, + + /// Reference (name and namespace) to a Kubernetes Secret object where workload + /// passwords will be stored. This must not be accessible to end users. + password_cache_secret: SecretReference, + + /// The root Distinguished Name (DN) where service accounts should be provisioned, + /// typically `CN=Users,{domain_dn}`. + user_distinguished_name: String, + + /// The root Distinguished Name (DN) for AD-managed schemas, + /// typically `CN=Schema,CN=Configuration,{domain_dn}`. + schema_distinguished_name: String, + + /// Allows samAccountName generation for new accounts to be customized. + /// Note that setting this field (even if empty) makes the Secret Operator take + /// over the generation duty from the domain controller. + #[serde(rename = "experimentalGenerateSamAccountName")] + generate_sam_account_name: Option, + }, + } + + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct ActiveDirectorySamAccountNameRules { + /// A prefix to be prepended to generated samAccountNames. + #[serde(default)] + pub prefix: String, + /// The total length of generated samAccountNames, _including_ `prefix`. + /// Must be larger than the length of `prefix`, but at most `20`. + /// + /// Note that this should be as large as possible, to minimize the risk of collisions. + #[serde(default = "ActiveDirectorySamAccountNameRules::default_total_length")] + pub total_length: u8, + } + + /// A [TrustStore](DOCS_BASE_URL_PLACEHOLDER/secret-operator/truststore) requests information about how to + /// validate secrets issued by a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass). + /// + /// The requested information is written to a ConfigMap with the same name as the TrustStore. + #[versioned(crd(group = "secrets.stackable.tech", namespaced))] + #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct TrustStoreSpec { + /// The name of the SecretClass that the request concerns. + pub secret_class_name: String, + + /// The [format](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#format) that the data should be converted into. + pub format: Option, + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + backend::tls::{DEFAULT_CA_CERT_LIFETIME, DEFAULT_MAX_CERT_LIFETIME}, + crd::v1alpha1::{ + AdditionalTrustRoot, AutoTlsBackend, AutoTlsCa, CertificateKeyGeneration, SecretClass, + SecretClassBackend, SecretClassSpec, + }, + }; + + #[test] + fn test_deserialization() { + let input: &str = r#" + apiVersion: secrets.stackable.tech/v1alpha1 + kind: SecretClass + metadata: + name: tls + spec: + backend: + autoTls: + ca: + secret: + name: secret-provisioner-tls-ca + namespace: default + keyGeneration: + rsa: + length: 3072 + "#; + let deserializer = serde_yaml::Deserializer::from_str(input); + let secret_class: SecretClass = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); + assert_eq!( + secret_class.spec, + SecretClassSpec { + backend: SecretClassBackend::AutoTls(AutoTlsBackend { + ca: AutoTlsCa { + secret: SecretReference { + name: "secret-provisioner-tls-ca".to_string(), + namespace: "default".to_string(), + }, + auto_generate: false, + ca_certificate_lifetime: DEFAULT_CA_CERT_LIFETIME, + key_generation: CertificateKeyGeneration::Rsa { + length: CertificateKeyGeneration::RSA_KEY_LENGTH_3072 + } + }, + additional_trust_roots: vec![], + max_certificate_lifetime: DEFAULT_MAX_CERT_LIFETIME, + }) + } + ); + + let input: &str = r#" + apiVersion: secrets.stackable.tech/v1alpha1 + kind: SecretClass + metadata: + name: tls + spec: + backend: + autoTls: + ca: + secret: + name: secret-provisioner-tls-ca + namespace: default + autoGenerate: true + caCertificateLifetime: 100d + additionalTrustRoots: + - configMap: + name: tls-root-ca-config-map + namespace: default + - secret: + name: tls-root-ca-secret + namespace: default + maxCertificateLifetime: 31d + "#; + let deserializer = serde_yaml::Deserializer::from_str(input); + let secret_class: SecretClass = + serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap(); + assert_eq!( + secret_class.spec, + SecretClassSpec { + backend: SecretClassBackend::AutoTls(AutoTlsBackend { + ca: AutoTlsCa { + secret: SecretReference { + name: "secret-provisioner-tls-ca".to_string(), + namespace: "default".to_string(), + }, + auto_generate: true, + ca_certificate_lifetime: Duration::from_days_unchecked(100), + key_generation: CertificateKeyGeneration::default() + }, + additional_trust_roots: vec![ + AdditionalTrustRoot::ConfigMap(ConfigMapReference { + name: "tls-root-ca-config-map".to_string(), + namespace: "default".to_string(), + }), + AdditionalTrustRoot::Secret(SecretReference { + name: "tls-root-ca-secret".to_string(), + namespace: "default".to_string(), + }) + ], + max_certificate_lifetime: Duration::from_days_unchecked(31), + }) + } + ); + } +} diff --git a/rust/operator-binary/src/crd/v1alpha1_impl.rs b/rust/operator-binary/src/crd/v1alpha1_impl.rs new file mode 100644 index 00000000..e3010ef8 --- /dev/null +++ b/rust/operator-binary/src/crd/v1alpha1_impl.rs @@ -0,0 +1,225 @@ +use std::{fmt::Display, ops::Deref}; + +use snafu::Snafu; +use stackable_operator::{ + k8s_openapi::api::core::v1::{ConfigMap, Secret}, + kube::api::PartialObjectMeta, + schemars::{self, schema::Schema}, + shared::time::Duration, +}; + +use crate::{ + backend, + crd::{ + KerberosPrincipal, + v1alpha1::{ + ActiveDirectorySamAccountNameRules, AutoTlsBackend, AutoTlsCa, CertManagerBackend, + CertificateKeyGeneration, SearchNamespace, SearchNamespaceMatchCondition, + SecretClassBackend, + }, + }, +}; + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum InvalidKerberosPrincipal { + #[snafu(display( + "principal contains illegal characters (allowed: alphanumeric, /, @, -, _, and .)" + ))] + IllegalCharacter, + + #[snafu(display("principal may not start with a dash"))] + StartWithDash, +} + +impl SecretClassBackend { + // Currently no `refers_to_*` method actually returns more than one element, + // but returning `Iterator` instead of `Option` to ensure that all consumers are ready + // for adding more conditions. + + // The matcher methods are on the CRD type rather than the initialized `Backend` impls + // to avoid having to initialize the backend for each watch event. + + /// Returns the conditions where the backend refers to `config_map`. + pub fn refers_to_config_map( + &self, + config_map: &PartialObjectMeta, + ) -> impl Iterator { + let cm_namespace = config_map.metadata.namespace.as_deref(); + match self { + Self::K8sSearch(backend) => { + let name_matches = backend.trust_store_config_map_name == config_map.metadata.name; + cm_namespace + .filter(|_| name_matches) + .and_then(|cm_ns| backend.search_namespace.matches_namespace(cm_ns)) + } + Self::AutoTls(_) => None, + Self::CertManager(_) => None, + Self::KerberosKeytab(_) => None, + } + .into_iter() + } + + /// Returns the conditions where the backend refers to `secret`. + pub fn refers_to_secret( + &self, + secret: &PartialObjectMeta, + ) -> impl Iterator { + match self { + Self::AutoTls(backend) => { + (backend.ca.secret == *secret).then_some(SearchNamespaceMatchCondition::True) + } + Self::K8sSearch(_) => None, + Self::CertManager(_) => None, + Self::KerberosKeytab(_) => None, + } + .into_iter() + } +} + +impl SearchNamespace { + pub fn resolve<'a>(&'a self, pod_namespace: &'a str) -> &'a str { + match self { + SearchNamespace::Pod {} => pod_namespace, + SearchNamespace::Name(ns) => ns, + } + } + + /// Returns [`Some`] if this `SearchNamespace` could possibly match an object in the namespace + /// `object_namespace`, otherwise [`None`]. + /// + /// This is optimistic, you then need to call [`SearchNamespaceMatchCondition::matches_pod_namespace`] + /// to evaluate the match for a specific pod's namespace. + pub fn matches_namespace( + &self, + object_namespace: &str, + ) -> Option { + match self { + SearchNamespace::Pod {} => Some(SearchNamespaceMatchCondition::IfPodIsInNamespace { + namespace: object_namespace.to_string(), + }), + SearchNamespace::Name(ns) => { + (ns == object_namespace).then_some(SearchNamespaceMatchCondition::True) + } + } + } +} + +impl SearchNamespaceMatchCondition { + pub fn matches_pod_namespace(&self, pod_ns: &str) -> bool { + match self { + Self::True => true, + Self::IfPodIsInNamespace { namespace } => namespace == pod_ns, + } + } +} + +impl AutoTlsBackend { + pub(crate) fn default_max_certificate_lifetime() -> Duration { + backend::tls::DEFAULT_MAX_CERT_LIFETIME + } +} + +impl AutoTlsCa { + pub(crate) fn default_ca_certificate_lifetime() -> Duration { + backend::tls::DEFAULT_CA_CERT_LIFETIME + } +} + +impl CertificateKeyGeneration { + pub const RSA_KEY_LENGTH_2048: u32 = 2048; + pub const RSA_KEY_LENGTH_3072: u32 = 3072; + pub const RSA_KEY_LENGTH_4096: u32 = 4096; + + // Could not get a "standard" enum with assigned values/discriminants to work as integers in the schema + // The following was generated and requires the length to be provided as string (we want an integer) + // keyGeneration: + // default: + // rsa: + // length: '2048' + // oneOf: + // - required: + // - rsa + // properties: + // rsa: + // properties: + // length: + // enum: + // - '2048' + // - '3072' + // - '4096' + // type: string + pub fn tls_key_length_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { + serde_json::from_value(serde_json::json!({ + "type": "integer", + "enum": [ + Self::RSA_KEY_LENGTH_2048, + Self::RSA_KEY_LENGTH_3072, + Self::RSA_KEY_LENGTH_4096 + ] + })) + .expect("Failed to parse JSON of custom tls key length schema") + } +} + +impl Default for CertificateKeyGeneration { + fn default() -> Self { + Self::Rsa { + length: Self::RSA_KEY_LENGTH_2048, + } + } +} + +impl CertManagerBackend { + pub(crate) fn default_certificate_lifetime() -> Duration { + backend::cert_manager::DEFAULT_CERT_LIFETIME + } +} + +impl ActiveDirectorySamAccountNameRules { + pub(crate) fn default_total_length() -> u8 { + // Default AD samAccountName length limit + 20 + } +} + +impl TryFrom for KerberosPrincipal { + type Error = InvalidKerberosPrincipal; + + fn try_from(value: String) -> Result { + if value.starts_with('-') { + invalid_kerberos_principal::StartWithDashSnafu.fail() + } else if value.contains(|chr: char| { + !chr.is_alphanumeric() + && chr != '/' + && chr != '@' + && chr != '.' + && chr != '-' + && chr != '_' + }) { + invalid_kerberos_principal::IllegalCharacterSnafu.fail() + } else { + Ok(KerberosPrincipal(value)) + } + } +} + +impl From for String { + fn from(value: KerberosPrincipal) -> Self { + value.0 + } +} + +impl Display for KerberosPrincipal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl Deref for KerberosPrincipal { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 2db1adc9..5c9b3b81 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -15,8 +15,9 @@ use grpc::csi::v1::{ controller_server::ControllerServer, identity_server::IdentityServer, node_server::NodeServer, }; use stackable_operator::{ - CustomResourceExt, + YamlSchema, cli::{CommonOptions, ProductOperatorRun}, + shared::yaml::SerializeOptions, telemetry::Tracing, }; use tokio::signal::unix::{SignalKind, signal}; @@ -24,6 +25,8 @@ use tokio_stream::wrappers::UnixListenerStream; use tonic::transport::Server; use utils::{TonicUnixStream, uds_bind_private}; +use crate::crd::{SecretClass, TrustStore}; + mod backend; mod crd; mod csi_server; @@ -69,8 +72,10 @@ async fn main() -> anyhow::Result<()> { let opts = Opts::parse(); match opts.cmd { stackable_operator::cli::Command::Crd => { - crd::SecretClass::print_yaml_schema(built_info::PKG_VERSION)?; - crd::TrustStore::print_yaml_schema(built_info::PKG_VERSION)?; + SecretClass::merged_crd(crd::SecretClassVersion::V1Alpha1)? + .print_yaml_schema(built_info::PKG_VERSION, SerializeOptions::default())?; + TrustStore::merged_crd(crd::TrustStoreVersion::V1Alpha1)? + .print_yaml_schema(built_info::PKG_VERSION, SerializeOptions::default())?; } stackable_operator::cli::Command::Run(SecretOperatorRun { csi_endpoint, diff --git a/rust/operator-binary/src/truststore_controller.rs b/rust/operator-binary/src/truststore_controller.rs index 89ff079f..3589a64a 100644 --- a/rust/operator-binary/src/truststore_controller.rs +++ b/rust/operator-binary/src/truststore_controller.rs @@ -32,7 +32,7 @@ use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ OPERATOR_NAME, backend::{self, SecretBackendError, TrustSelector}, - crd::{SearchNamespaceMatchCondition, SecretClass, TrustStore}, + crd::v1alpha1, format::{ self, well_known::{CompatibilityOptions, NamingOptions}, @@ -46,7 +46,7 @@ const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, ".", OPERATOR_NAME pub async fn start(client: &stackable_operator::client::Client, watch_namespace: &WatchNamespace) { let (secretclasses, secretclasses_writer) = reflector::store(); let controller = Controller::new( - watch_namespace.get_api::>(client), + watch_namespace.get_api::>(client), watcher::Config::default(), ); let truststores = controller.store(); @@ -60,7 +60,7 @@ pub async fn start(client: &stackable_operator::client::Client, watch_namespace: controller .watches_stream( watcher( - client.get_api::>(&()), + client.get_api::>(&()), watcher::Config::default(), ) .reflect(secretclasses_writer) @@ -119,29 +119,33 @@ pub async fn start(client: &stackable_operator::client::Client, watch_namespace: .await; } -/// Resolves modifications to dependencies of [`SecretClass`] objects into -/// a list of affected [`TrustStore`]s. +/// Resolves modifications to dependencies of [`v1alpha1::SecretClass`] objects into +/// a list of affected [`v1alpha1::TrustStore`]s. fn secretclass_dependency_watch_mapper( - truststores: reflector::Store>, - secretclasses: reflector::Store>, - reference_conditions: impl Copy + Fn(&SecretClass, &Dep) -> Conds, -) -> impl Fn(Dep) -> Vec>> + truststores: reflector::Store>, + secretclasses: reflector::Store>, + reference_conditions: impl Copy + Fn(&v1alpha1::SecretClass, &Dep) -> Conds, +) -> impl Fn(Dep) -> Vec>> where - Conds: IntoIterator, + Conds: IntoIterator, { move |dep| { - let potentially_matching_secretclasses = secretclasses - .state() - .into_iter() - .filter_map(move |sc| { - sc.0.as_ref().ok().and_then(|sc| { - let conditions = reference_conditions(sc, &dep) - .into_iter() - .collect::>(); - (!conditions.is_empty()).then(|| (ObjectRef::from_obj(sc), conditions)) + let potentially_matching_secretclasses = + secretclasses + .state() + .into_iter() + .filter_map(move |sc| { + sc.0.as_ref().ok().and_then(|sc| { + let conditions = reference_conditions(sc, &dep) + .into_iter() + .collect::>(); + (!conditions.is_empty()).then(|| (ObjectRef::from_obj(sc), conditions)) + }) }) - }) - .collect::, Vec>>(); + .collect::, + Vec, + >>(); truststores .state() .into_iter() @@ -151,7 +155,7 @@ where return false; }; let secret_class_ref = - ObjectRef::::new(&ts.spec.secret_class_name); + ObjectRef::::new(&ts.spec.secret_class_name); potentially_matching_secretclasses .get(&secret_class_ref) .is_some_and(|conds| { @@ -177,13 +181,13 @@ pub enum Error { #[snafu(display("failed to get {secret_class} for TrustStore"))] GetSecretClass { source: stackable_operator::client::Error, - secret_class: ObjectRef, + secret_class: ObjectRef, }, #[snafu(display("failed to initialize SecretClass backend for {secret_class}"))] InitBackend { source: backend::dynamic::FromClassError, - secret_class: ObjectRef, + secret_class: ObjectRef, }, #[snafu(display("failed to get trust data from backend"))] @@ -195,7 +199,7 @@ pub enum Error { #[snafu(display("failed to convert trust data into desired format"))] FormatData { source: format::IntoFilesError, - secret_class: ObjectRef, + secret_class: ObjectRef, }, #[snafu(display("failed to build owner reference to the TrustStore"))] @@ -234,7 +238,7 @@ struct Ctx { } async fn reconcile( - truststore: Arc>, + truststore: Arc>, ctx: Arc, ) -> Result { let truststore = truststore @@ -245,10 +249,10 @@ async fn reconcile( let secret_class_name = &truststore.spec.secret_class_name; let secret_class = ctx .client - .get::(secret_class_name, &()) + .get::(secret_class_name, &()) .await .context(GetSecretClassSnafu { - secret_class: ObjectRef::::new(secret_class_name), + secret_class: ObjectRef::::new(secret_class_name), })?; let secret_class_ref = secret_class.to_object_ref(()); let backend = backend::dynamic::from_class(&ctx.client, secret_class) @@ -304,7 +308,7 @@ async fn reconcile( } fn error_policy( - _obj: Arc>, + _obj: Arc>, _error: &Error, _ctx: Arc, ) -> controller::Action {