From fd0494557671469dbb136f01be02eef9ca281961 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 26 Feb 2025 12:47:51 +0100 Subject: [PATCH 01/22] chore: Add stackable-versioned dependency --- Cargo.lock | 1 + crates/stackable-operator/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index c0872715d..4be4f76ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3226,6 +3226,7 @@ dependencies = [ "snafu 0.8.5", "stackable-operator-derive", "stackable-shared", + "stackable-versioned", "strum", "tempfile", "time", diff --git a/crates/stackable-operator/Cargo.toml b/crates/stackable-operator/Cargo.toml index 468b25fa1..8aaef07d8 100644 --- a/crates/stackable-operator/Cargo.toml +++ b/crates/stackable-operator/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true time = ["dep:time"] [dependencies] +stackable-versioned = { path = "../stackable-versioned", features = ["k8s"] } stackable-operator-derive = { path = "../stackable-operator-derive" } stackable-shared = { path = "../stackable-shared" } From ca09bb0ca8dcee443f3b4e92517c671761f3f151 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 26 Feb 2025 12:48:04 +0100 Subject: [PATCH 02/22] chore: Move crd.rs file into module folder --- crates/stackable-operator/src/{crd.rs => crd/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/stackable-operator/src/{crd.rs => crd/mod.rs} (100%) diff --git a/crates/stackable-operator/src/crd.rs b/crates/stackable-operator/src/crd/mod.rs similarity index 100% rename from crates/stackable-operator/src/crd.rs rename to crates/stackable-operator/src/crd/mod.rs From 42d3c9c1cb0cdc5b93cc1a2c2d1506f744bc6a4a Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 26 Feb 2025 14:12:54 +0100 Subject: [PATCH 03/22] chore: Move code into crd module --- .../src/cluster_resources.rs | 2 +- .../src/commons/listener.rs | 288 ------------------ crates/stackable-operator/src/commons/mod.rs | 3 - .../stackable-operator/src/commons/s3/crd.rs | 158 ---------- .../stackable-operator/src/commons/s3/mod.rs | 43 --- .../src/commons/tls_verification.rs | 6 +- .../stackable-operator/src/constants/mod.rs | 1 + .../src/constants/secret.rs | 1 + .../authentication/kerberos.rs | 0 .../{commons => crd}/authentication/ldap.rs | 2 +- .../{commons => crd}/authentication/mod.rs | 15 +- .../{commons => crd}/authentication/oidc.rs | 7 +- .../authentication/static_.rs | 0 .../{commons => crd}/authentication/tls.rs | 0 .../src/crd/listener/class.rs | 59 ++++ .../src/crd/listener/listeners.rs | 132 ++++++++ .../src/crd/listener/mod.rs | 116 +++++++ crates/stackable-operator/src/crd/mod.rs | 4 + .../stackable-operator/src/crd/s3/bucket.rs | 102 +++++++ .../s3/helpers.rs => crd/s3/connection.rs} | 257 +++++++++++----- crates/stackable-operator/src/crd/s3/mod.rs | 5 + crates/stackable-operator/src/lib.rs | 1 + 22 files changed, 622 insertions(+), 580 deletions(-) delete mode 100644 crates/stackable-operator/src/commons/listener.rs delete mode 100644 crates/stackable-operator/src/commons/s3/crd.rs delete mode 100644 crates/stackable-operator/src/commons/s3/mod.rs create mode 100644 crates/stackable-operator/src/constants/mod.rs create mode 100644 crates/stackable-operator/src/constants/secret.rs rename crates/stackable-operator/src/{commons => crd}/authentication/kerberos.rs (100%) rename crates/stackable-operator/src/{commons => crd}/authentication/ldap.rs (99%) rename crates/stackable-operator/src/{commons => crd}/authentication/mod.rs (94%) rename crates/stackable-operator/src/{commons => crd}/authentication/oidc.rs (98%) rename crates/stackable-operator/src/{commons => crd}/authentication/static_.rs (100%) rename crates/stackable-operator/src/{commons => crd}/authentication/tls.rs (100%) create mode 100644 crates/stackable-operator/src/crd/listener/class.rs create mode 100644 crates/stackable-operator/src/crd/listener/listeners.rs create mode 100644 crates/stackable-operator/src/crd/listener/mod.rs create mode 100644 crates/stackable-operator/src/crd/s3/bucket.rs rename crates/stackable-operator/src/{commons/s3/helpers.rs => crd/s3/connection.rs} (53%) create mode 100644 crates/stackable-operator/src/crd/s3/mod.rs diff --git a/crates/stackable-operator/src/cluster_resources.rs b/crates/stackable-operator/src/cluster_resources.rs index dd4fad75c..0ff6af005 100644 --- a/crates/stackable-operator/src/cluster_resources.rs +++ b/crates/stackable-operator/src/cluster_resources.rs @@ -33,12 +33,12 @@ use crate::{ client::{Client, GetApi}, commons::{ cluster_operation::ClusterOperation, - listener::Listener, resources::{ ComputeResource, ResourceRequirementsExt, ResourceRequirementsType, LIMIT_REQUEST_RATIO_CPU, LIMIT_REQUEST_RATIO_MEMORY, }, }, + crd::listener::Listener, kvp::{ consts::{K8S_APP_INSTANCE_KEY, K8S_APP_MANAGED_BY_KEY, K8S_APP_NAME_KEY}, Label, LabelError, Labels, diff --git a/crates/stackable-operator/src/commons/listener.rs b/crates/stackable-operator/src/commons/listener.rs deleted file mode 100644 index b088e3950..000000000 --- a/crates/stackable-operator/src/commons/listener.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! This modules provides resource types used to interact with [listener-operator](https://docs.stackable.tech/listener-operator/stable/index.html) -//! -//! # Custom Resources -//! -//! ## [`Listener`] -//! -//! Exposes a set of pods, either internally to the cluster or to the outside world. The mechanism for how it is exposed -//! is managed by the [`ListenerClass`]. -//! -//! It can be either created manually by the application administrator (for applications that expose a single load-balanced endpoint), -//! or automatically when mounting a [listener volume](`ListenerOperatorVolumeSourceBuilder`) (for applications that expose a separate endpoint -//! per replica). -//! -//! All exposed pods *must* have a mounted [listener volume](`ListenerOperatorVolumeSourceBuilder`), regardless of whether the [`Listener`] is created automatically. -//! -//! ## [`ListenerClass`] -//! -//! Declares a policy for how [`Listener`]s are exposed to users. -//! -//! It is created by the cluster administrator. -//! -//! ## [`PodListeners`] -//! -//! Informs users and other operators about the state of all [`Listener`]s associated with a [`Pod`]. -//! -//! It is created by the Stackable Secret Operator, and always named `pod-{pod.metadata.uid}`. - -use std::collections::BTreeMap; - -#[cfg(doc)] -use k8s_openapi::api::core::v1::{ - Node, PersistentVolume, PersistentVolumeClaim, Pod, Service, Volume, -}; -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[cfg(doc)] -use crate::builder::pod::volume::ListenerOperatorVolumeSourceBuilder; - -/// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed. -/// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass) -/// for more information. -#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[kube( - group = "listeners.stackable.tech", - version = "v1alpha1", - kind = "ListenerClass" -)] -#[serde(rename_all = "camelCase")] -pub struct ListenerClassSpec { - pub service_type: ServiceType, - - /// Annotations that should be added to the Service object. - #[serde(default)] - pub service_annotations: BTreeMap, - - /// `externalTrafficPolicy` that should be set on the created [`Service`] objects. - /// - /// The default is `Local` (in contrast to `Cluster`), as we aim to direct traffic to a node running the workload - /// and we should keep testing that as the primary configuration. Cluster is a fallback option for providers that - /// break Local mode (IONOS so far). - #[serde(default = "ListenerClassSpec::default_service_external_traffic_policy")] - pub service_external_traffic_policy: KubernetesTrafficPolicy, - - /// Whether addresses should prefer using the IP address (`IP`) or the hostname (`Hostname`). - /// Can also be set to `HostnameConservative`, which will use `IP` for `NodePort` service types, but `Hostname` for everything else. - /// - /// The other type will be used if the preferred type is not available. - /// - /// Defaults to `HostnameConservative`. - #[serde(default = "ListenerClassSpec::default_preferred_address_type")] - pub preferred_address_type: PreferredAddressType, -} - -impl ListenerClassSpec { - const fn default_service_external_traffic_policy() -> KubernetesTrafficPolicy { - KubernetesTrafficPolicy::Local - } - - const fn default_preferred_address_type() -> PreferredAddressType { - PreferredAddressType::HostnameConservative - } - - /// Resolves [`Self::preferred_address_type`]'s "smart" modes depending on the rest of `self`. - pub fn resolve_preferred_address_type(&self) -> AddressType { - self.preferred_address_type.resolve(self) - } -} - -/// The method used to access the services. -// -// Please note that this does not necessarely need to be restricted to the same Service types Kubernetes supports. -// Listeners currently happens to support the same set of service types as upstream Kubernetes, but we still want to -// have the freedom to add custom ones in the future (for example: Istio ingress?). -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -pub enum ServiceType { - /// Reserve a port on each node. - NodePort, - - /// Provision a dedicated load balancer. - LoadBalancer, - - /// Assigns an IP address from a pool of IP addresses that your cluster has reserved for that purpose. - ClusterIP, -} - -/// Service Internal Traffic Policy enables internal traffic restrictions to only route internal traffic to endpoints -/// within the node the traffic originated from. The "internal" traffic here refers to traffic originated from Pods in -/// the current cluster. This can help to reduce costs and improve performance. -/// See [Kubernetes docs](https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/). -// -// Please note that this represents a Kubernetes type, so the name of the enum variant needs to exactly match the -// Kubernetes traffic policy. -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq, strum::Display)] -pub enum KubernetesTrafficPolicy { - /// Obscures the client source IP and may cause a second hop to another node, but allows Kubernetes to spread the load between all nodes. - Cluster, - - /// Preserves the client source IP and avoid a second hop for LoadBalancer and NodePort type Services, but makes clients responsible for spreading the load. - Local, -} - -/// Exposes a set of pods to the outside world. -/// -/// Essentially a Stackable extension of a Kubernetes Service. Compared to a Service, a Listener changes three things: -/// 1. It uses a cluster-level policy object (ListenerClass) to define how exactly the exposure works -/// 2. It has a consistent API for reading back the exposed address(es) of the service -/// 3. The Pod must mount a Volume referring to the Listener, which also allows -/// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling). -/// -/// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener). -#[derive( - CustomResource, Serialize, Deserialize, Default, Clone, Debug, JsonSchema, PartialEq, Eq, -)] -#[kube( - group = "listeners.stackable.tech", - version = "v1alpha1", - kind = "Listener", - namespaced, - status = "ListenerStatus" -)] -#[serde(rename_all = "camelCase")] -pub struct ListenerSpec { - /// The name of the [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass). - pub class_name: Option, - - /// Extra labels that the Pods must match in order to be exposed. They must _also_ still have a Volume referring to the Listener. - #[serde(default)] - pub extra_pod_selector_labels: BTreeMap, - - /// Ports that should be exposed. - pub ports: Option>, - - /// Whether incoming traffic should also be directed to Pods that are not `Ready`. - #[serde(default = "ListenerSpec::default_publish_not_ready_addresses")] - pub publish_not_ready_addresses: Option, -} - -impl ListenerSpec { - const fn default_publish_not_ready_addresses() -> Option { - Some(true) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ListenerPort { - /// The name of the port. - /// - /// The name of each port *must* be unique within a single Listener. - pub name: String, - /// The port number. - pub port: i32, - /// The layer-4 protocol (`TCP` or `UDP`). - pub protocol: Option, -} - -/// Informs users about how to reach the Listener. -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ListenerStatus { - /// The backing Kubernetes Service. - pub service_name: Option, - /// All addresses that the Listener is currently reachable from. - pub ingress_addresses: Option>, - /// Port mappings for accessing the Listener on each Node that the Pods are currently running on. - /// - /// This is only intended for internal use by listener-operator itself. This will be left unset if using a ListenerClass that does - /// not require Node-local access. - pub node_ports: Option>, -} - -/// One address that a Listener is accessible from. -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ListenerIngress { - /// The hostname or IP address to the Listener. - pub address: String, - /// The type of address (`Hostname` or `IP`). - pub address_type: AddressType, - /// Port mapping table. - pub ports: BTreeMap, -} - -/// The type of a given address. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "PascalCase")] -pub enum AddressType { - /// A resolvable DNS hostname. - Hostname, - - /// A resolved IP address. - #[serde(rename = "IP")] - Ip, -} - -/// A mode for deciding the preferred [`AddressType`]. -/// -/// These can vary depending on the rest of the [`ListenerClass`]. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -pub enum PreferredAddressType { - /// Like [`AddressType::Hostname`], but prefers [`AddressType::Ip`] for [`ServiceType::NodePort`], since their hostnames are less likely to be resolvable. - HostnameConservative, - - // Like the respective variants of AddressType. Ideally we would refer to them instead of copy/pasting, but that breaks due to upstream issues: - // - https://github.com/GREsau/schemars/issues/222 - // - https://github.com/kube-rs/kube/issues/1622 - Hostname, - #[serde(rename = "IP")] - Ip, -} - -impl PreferredAddressType { - pub fn resolve(self, listener_class: &ListenerClassSpec) -> AddressType { - match self { - PreferredAddressType::HostnameConservative => match listener_class.service_type { - ServiceType::NodePort => AddressType::Ip, - _ => AddressType::Hostname, - }, - PreferredAddressType::Hostname => AddressType::Hostname, - PreferredAddressType::Ip => AddressType::Ip, - } - } -} - -/// Informs users about Listeners that are bound by a given Pod. -/// -/// This is not expected to be created or modified by users. It will be created by -/// the Stackable Listener Operator when mounting the listener volume, and is always -/// named `pod-{pod.metadata.uid}`. -#[derive( - CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, Default, PartialEq, Eq, -)] -#[kube( - group = "listeners.stackable.tech", - version = "v1alpha1", - kind = "PodListeners", - namespaced, - plural = "podlisteners" -)] -#[serde(rename_all = "camelCase")] -pub struct PodListenersSpec { - /// All Listeners currently bound by the Pod. - /// - /// Indexed by Volume name (not PersistentVolume or PersistentVolumeClaim). - pub listeners: BTreeMap, -} - -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct PodListener { - /// `Node` if this address only allows access to Pods hosted on a specific Kubernetes Node, otherwise `Cluster`. - pub scope: PodListenerScope, - /// Addresses allowing access to this Pod. - /// - /// Compared to `ingress_addresses` on the Listener status, this list is restricted to addresses that can access this Pod. - /// - /// This field is intended to be equivalent to the files mounted into the Listener volume. - pub ingress_addresses: Option>, -} - -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "PascalCase")] -pub enum PodListenerScope { - Node, - Cluster, -} diff --git a/crates/stackable-operator/src/commons/mod.rs b/crates/stackable-operator/src/commons/mod.rs index b29d078ae..21a778f83 100644 --- a/crates/stackable-operator/src/commons/mod.rs +++ b/crates/stackable-operator/src/commons/mod.rs @@ -1,17 +1,14 @@ //! This module provides common datastructures or CRDs shared between all the operators pub mod affinity; -pub mod authentication; pub mod cache; pub mod cluster_operation; -pub mod listener; pub mod networking; pub mod opa; pub mod pdb; pub mod product_image_selection; pub mod rbac; pub mod resources; -pub mod s3; pub mod secret; pub mod secret_class; pub mod tls_verification; diff --git a/crates/stackable-operator/src/commons/s3/crd.rs b/crates/stackable-operator/src/commons/s3/crd.rs deleted file mode 100644 index 9a451b417..000000000 --- a/crates/stackable-operator/src/commons/s3/crd.rs +++ /dev/null @@ -1,158 +0,0 @@ -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::commons::{ - networking::HostName, s3::S3ConnectionInlineOrReference, secret_class::SecretClassVolume, - tls_verification::TlsClientDetails, -}; - -/// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. -/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). -#[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[kube( - group = "s3.stackable.tech", - version = "v1alpha1", - kind = "S3Bucket", - plural = "s3buckets", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced -)] -#[serde(rename_all = "camelCase")] -pub struct S3BucketSpec { - /// The name of the S3 bucket. - pub bucket_name: String, - - /// The definition of an S3 connection, either inline or as a reference. - pub connection: S3ConnectionInlineOrReference, -} - -/// S3 connection definition as a resource. -/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). -#[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[kube( - group = "s3.stackable.tech", - version = "v1alpha1", - kind = "S3Connection", - plural = "s3connections", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced -)] -#[serde(rename_all = "camelCase")] -pub struct S3ConnectionSpec { - /// Host of the S3 server without any protocol or port. For example: `west1.my-cloud.com`. - pub host: HostName, - - /// Port the S3 server listens on. - /// If not specified the product will determine the port to use. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub port: Option, - - /// AWS service API region used by the AWS SDK when using AWS S3 buckets. - /// - /// This defaults to `us-east-1` and can be ignored if not using AWS S3 - /// buckets. - /// - /// NOTE: This is not the bucket region, and is used by the AWS SDK to - /// construct endpoints for various AWS service APIs. It is only useful when - /// using AWS S3 buckets. - /// - /// When using AWS S3 buckets, you can configure optimal AWS service API - /// connections in the following ways: - /// - From **inside** AWS: Use an auto-discovery source (eg: AWS IMDS). - /// - From **outside** AWS, or when IMDS is disabled, explicity set the - /// region name nearest to where the client application is running from. - #[serde(default)] - pub region: AwsRegion, - - /// Which access style to use. - /// Defaults to virtual hosted-style as most of the data products out there. - /// Have a look at the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html). - #[serde(default)] - pub access_style: S3AccessStyle, - - /// If the S3 uses authentication you have to specify you S3 credentials. - /// In the most cases a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) - /// providing `accessKey` and `secretKey` is sufficient. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub credentials: Option, - - /// Use a TLS connection. If not specified no TLS will be used. - #[serde(flatten)] - pub tls: TlsClientDetails, -} - -#[derive( - strum::Display, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, -)] -#[strum(serialize_all = "PascalCase")] -pub enum S3AccessStyle { - /// Use path-style access as described in - Path, - - /// Use as virtual hosted-style access as described in - #[default] - VirtualHosted, -} - -/// Set a named AWS region, or defer to an auto-discovery mechanism. -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum AwsRegion { - /// Defer region detection to an auto-discovery mechanism. - Source(AwsRegionAutoDiscovery), - - /// An explicit region, eg: eu-central-1 - Name(String), -} - -impl AwsRegion { - /// Get the AWS region name. - /// - /// Returns `None` if an auto-discovery source has been selected. Otherwise, - /// it returns the configured region name. - /// - /// Example usage: - /// - /// ``` - /// # use stackable_operator::commons::s3::AwsRegion; - /// # fn set_property(key: &str, value: &str) {} - /// # fn example(aws_region: AwsRegion) { - /// if let Some(region_name) = aws_region.name() { - /// // set some property if the region is set, or is the default. - /// set_property("aws.region", region_name); - /// }; - /// # } - /// ``` - pub fn name(&self) -> Option<&str> { - match self { - AwsRegion::Name(name) => Some(name), - AwsRegion::Source(_) => None, - } - } -} - -impl Default for AwsRegion { - fn default() -> Self { - Self::Name("us-east-1".to_owned()) - } -} - -/// AWS region auto-discovery mechanism. -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "PascalCase")] -pub enum AwsRegionAutoDiscovery { - /// AWS Instance Meta Data Service. - /// - /// This variant should result in no region being given to the AWS SDK, - /// which should, in turn, query the AWS IMDS. - AwsImds, -} diff --git a/crates/stackable-operator/src/commons/s3/mod.rs b/crates/stackable-operator/src/commons/s3/mod.rs deleted file mode 100644 index 8ed68d810..000000000 --- a/crates/stackable-operator/src/commons/s3/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -mod crd; -mod helpers; - -pub use crd::*; -pub use helpers::*; -use snafu::Snafu; -use url::Url; - -use crate::commons::{ - secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError, -}; - -#[derive(Debug, Snafu)] -pub enum S3Error { - #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] - RetrieveS3Connection { - source: crate::client::Error, - s3_connection: String, - }, - - #[snafu(display("failed to parse S3 endpoint '{endpoint}'"))] - ParseS3Endpoint { - source: url::ParseError, - endpoint: String, - }, - - #[snafu(display("failed to set S3 endpoint scheme '{scheme}' for endpoint '{endpoint}'"))] - SetS3EndpointScheme { endpoint: Url, scheme: String }, - - #[snafu(display("failed to add S3 credential volumes and volume mounts"))] - AddS3CredentialVolumes { source: SecretClassVolumeError }, - - #[snafu(display("failed to add S3 TLS client details volumes and volume mounts"))] - AddS3TlsClientDetailsVolumes { source: TlsClientDetailsError }, - - #[snafu(display("failed to add required volumes"))] - AddVolumes { source: crate::builder::pod::Error }, - - #[snafu(display("failed to add required volumeMounts"))] - AddVolumeMounts { - source: crate::builder::pod::container::Error, - }, -} diff --git a/crates/stackable-operator/src/commons/tls_verification.rs b/crates/stackable-operator/src/commons/tls_verification.rs index 0123a6bad..0b5f8d859 100644 --- a/crates/stackable-operator/src/commons/tls_verification.rs +++ b/crates/stackable-operator/src/commons/tls_verification.rs @@ -8,10 +8,8 @@ use crate::{ self, pod::{container::ContainerBuilder, volume::VolumeMountBuilder, PodBuilder}, }, - commons::{ - authentication::SECRET_BASE_PATH, - secret_class::{SecretClassVolume, SecretClassVolumeError}, - }, + commons::secret_class::{SecretClassVolume, SecretClassVolumeError}, + constants::secret::SECRET_BASE_PATH, }; #[derive(Debug, Snafu)] diff --git a/crates/stackable-operator/src/constants/mod.rs b/crates/stackable-operator/src/constants/mod.rs new file mode 100644 index 000000000..73b12dbbd --- /dev/null +++ b/crates/stackable-operator/src/constants/mod.rs @@ -0,0 +1 @@ +pub mod secret; diff --git a/crates/stackable-operator/src/constants/secret.rs b/crates/stackable-operator/src/constants/secret.rs new file mode 100644 index 000000000..d728e5e3a --- /dev/null +++ b/crates/stackable-operator/src/constants/secret.rs @@ -0,0 +1 @@ +pub(crate) const SECRET_BASE_PATH: &str = "/stackable/secrets"; diff --git a/crates/stackable-operator/src/commons/authentication/kerberos.rs b/crates/stackable-operator/src/crd/authentication/kerberos.rs similarity index 100% rename from crates/stackable-operator/src/commons/authentication/kerberos.rs rename to crates/stackable-operator/src/crd/authentication/kerberos.rs diff --git a/crates/stackable-operator/src/commons/authentication/ldap.rs b/crates/stackable-operator/src/crd/authentication/ldap.rs similarity index 99% rename from crates/stackable-operator/src/commons/authentication/ldap.rs rename to crates/stackable-operator/src/crd/authentication/ldap.rs index 2657c05e4..059548078 100644 --- a/crates/stackable-operator/src/commons/authentication/ldap.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap.rs @@ -10,11 +10,11 @@ use crate::{ pod::{container::ContainerBuilder, volume::VolumeMountBuilder, PodBuilder}, }, commons::{ - authentication::SECRET_BASE_PATH, networking::HostName, secret_class::{SecretClassVolume, SecretClassVolumeError}, tls_verification::{TlsClientDetails, TlsClientDetailsError}, }, + constants::secret::SECRET_BASE_PATH, }; pub type Result = std::result::Result; diff --git a/crates/stackable-operator/src/commons/authentication/mod.rs b/crates/stackable-operator/src/crd/authentication/mod.rs similarity index 94% rename from crates/stackable-operator/src/commons/authentication/mod.rs rename to crates/stackable-operator/src/crd/authentication/mod.rs index bf5563b92..d9a69ee1f 100644 --- a/crates/stackable-operator/src/commons/authentication/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/mod.rs @@ -12,8 +12,6 @@ pub mod oidc; pub mod static_; pub mod tls; -pub(crate) const SECRET_BASE_PATH: &str = "/stackable/secrets"; - type Result = std::result::Result; #[derive(Debug, PartialEq, Snafu)] @@ -179,22 +177,19 @@ impl ClientAuthenticationDetails { #[cfg(test)] mod tests { - use crate::commons::authentication::{ - tls::AuthenticationProvider, AuthenticationClassProvider, - }; + use crate::crd::authentication::{kerberos, tls, AuthenticationClassProvider}; #[test] fn provider_to_string() { - let tls_provider = AuthenticationClassProvider::Tls(AuthenticationProvider { + let tls_provider = AuthenticationClassProvider::Tls(tls::AuthenticationProvider { client_cert_secret_class: None, }); assert_eq!("Tls", tls_provider.to_string()); - let kerberos_provider = AuthenticationClassProvider::Kerberos( - crate::commons::authentication::kerberos::AuthenticationProvider { + let kerberos_provider = + AuthenticationClassProvider::Kerberos(kerberos::AuthenticationProvider { kerberos_secret_class: "kerberos".to_string(), - }, - ); + }); assert_eq!("Kerberos", kerberos_provider.to_string()); } } diff --git a/crates/stackable-operator/src/commons/authentication/oidc.rs b/crates/stackable-operator/src/crd/authentication/oidc.rs similarity index 98% rename from crates/stackable-operator/src/commons/authentication/oidc.rs rename to crates/stackable-operator/src/crd/authentication/oidc.rs index ed19eebdb..8f19ee173 100644 --- a/crates/stackable-operator/src/commons/authentication/oidc.rs +++ b/crates/stackable-operator/src/crd/authentication/oidc.rs @@ -10,9 +10,10 @@ use snafu::{ResultExt, Snafu}; use url::{ParseError, Url}; #[cfg(doc)] -use crate::commons::authentication::AuthenticationClass; -use crate::commons::{ - authentication::SECRET_BASE_PATH, networking::HostName, tls_verification::TlsClientDetails, +use crate::crd::authentication::AuthenticationClass; +use crate::{ + commons::{networking::HostName, tls_verification::TlsClientDetails}, + constants::secret::SECRET_BASE_PATH, }; pub type Result = std::result::Result; diff --git a/crates/stackable-operator/src/commons/authentication/static_.rs b/crates/stackable-operator/src/crd/authentication/static_.rs similarity index 100% rename from crates/stackable-operator/src/commons/authentication/static_.rs rename to crates/stackable-operator/src/crd/authentication/static_.rs diff --git a/crates/stackable-operator/src/commons/authentication/tls.rs b/crates/stackable-operator/src/crd/authentication/tls.rs similarity index 100% rename from crates/stackable-operator/src/commons/authentication/tls.rs rename to crates/stackable-operator/src/crd/authentication/tls.rs diff --git a/crates/stackable-operator/src/crd/listener/class.rs b/crates/stackable-operator/src/crd/listener/class.rs new file mode 100644 index 000000000..196ee4a78 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/class.rs @@ -0,0 +1,59 @@ +use std::collections::BTreeMap; + +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::crd::listener::{ + AddressType, KubernetesTrafficPolicy, PreferredAddressType, ServiceType, +}; + +/// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed. +/// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass) +/// for more information. +#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] +#[kube( + group = "listeners.stackable.tech", + version = "v1alpha1", + kind = "ListenerClass" +)] +#[serde(rename_all = "camelCase")] +pub struct ListenerClassSpec { + pub service_type: ServiceType, + + /// Annotations that should be added to the Service object. + #[serde(default)] + pub service_annotations: BTreeMap, + + /// `externalTrafficPolicy` that should be set on the created [`Service`] objects. + /// + /// The default is `Local` (in contrast to `Cluster`), as we aim to direct traffic to a node running the workload + /// and we should keep testing that as the primary configuration. Cluster is a fallback option for providers that + /// break Local mode (IONOS so far). + #[serde(default = "ListenerClassSpec::default_service_external_traffic_policy")] + pub service_external_traffic_policy: KubernetesTrafficPolicy, + + /// Whether addresses should prefer using the IP address (`IP`) or the hostname (`Hostname`). + /// Can also be set to `HostnameConservative`, which will use `IP` for `NodePort` service types, but `Hostname` for everything else. + /// + /// The other type will be used if the preferred type is not available. + /// + /// Defaults to `HostnameConservative`. + #[serde(default = "ListenerClassSpec::default_preferred_address_type")] + pub preferred_address_type: PreferredAddressType, +} + +impl ListenerClassSpec { + const fn default_service_external_traffic_policy() -> KubernetesTrafficPolicy { + KubernetesTrafficPolicy::Local + } + + const fn default_preferred_address_type() -> PreferredAddressType { + PreferredAddressType::HostnameConservative + } + + /// Resolves [`Self::preferred_address_type`]'s "smart" modes depending on the rest of `self`. + pub fn resolve_preferred_address_type(&self) -> AddressType { + self.preferred_address_type.resolve(self) + } +} diff --git a/crates/stackable-operator/src/crd/listener/listeners.rs b/crates/stackable-operator/src/crd/listener/listeners.rs new file mode 100644 index 000000000..cf696c41f --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/listeners.rs @@ -0,0 +1,132 @@ +use std::collections::BTreeMap; + +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::crd::listener::AddressType; + +/// Exposes a set of pods to the outside world. +/// +/// Essentially a Stackable extension of a Kubernetes Service. Compared to a Service, a Listener changes three things: +/// 1. It uses a cluster-level policy object (ListenerClass) to define how exactly the exposure works +/// 2. It has a consistent API for reading back the exposed address(es) of the service +/// 3. The Pod must mount a Volume referring to the Listener, which also allows +/// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling). +/// +/// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener). +#[derive( + CustomResource, Serialize, Deserialize, Default, Clone, Debug, JsonSchema, PartialEq, Eq, +)] +#[kube( + group = "listeners.stackable.tech", + version = "v1alpha1", + kind = "Listener", + namespaced, + status = "ListenerStatus" +)] +#[serde(rename_all = "camelCase")] +pub struct ListenerSpec { + /// The name of the [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass). + pub class_name: Option, + + /// Extra labels that the Pods must match in order to be exposed. They must _also_ still have a Volume referring to the Listener. + #[serde(default)] + pub extra_pod_selector_labels: BTreeMap, + + /// Ports that should be exposed. + pub ports: Option>, + + /// Whether incoming traffic should also be directed to Pods that are not `Ready`. + #[serde(default = "ListenerSpec::default_publish_not_ready_addresses")] + pub publish_not_ready_addresses: Option, +} + +impl ListenerSpec { + const fn default_publish_not_ready_addresses() -> Option { + Some(true) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ListenerPort { + /// The name of the port. + /// + /// The name of each port *must* be unique within a single Listener. + pub name: String, + /// The port number. + pub port: i32, + /// The layer-4 protocol (`TCP` or `UDP`). + pub protocol: Option, +} + +/// Informs users about how to reach the Listener. +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ListenerStatus { + /// The backing Kubernetes Service. + pub service_name: Option, + /// All addresses that the Listener is currently reachable from. + pub ingress_addresses: Option>, + /// Port mappings for accessing the Listener on each Node that the Pods are currently running on. + /// + /// This is only intended for internal use by listener-operator itself. This will be left unset if using a ListenerClass that does + /// not require Node-local access. + pub node_ports: Option>, +} + +/// One address that a Listener is accessible from. +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ListenerIngress { + /// The hostname or IP address to the Listener. + pub address: String, + /// The type of address (`Hostname` or `IP`). + pub address_type: AddressType, + /// Port mapping table. + pub ports: BTreeMap, +} + +/// Informs users about Listeners that are bound by a given Pod. +/// +/// This is not expected to be created or modified by users. It will be created by +/// the Stackable Listener Operator when mounting the listener volume, and is always +/// named `pod-{pod.metadata.uid}`. +#[derive( + CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, Default, PartialEq, Eq, +)] +#[kube( + group = "listeners.stackable.tech", + version = "v1alpha1", + kind = "PodListeners", + namespaced, + plural = "podlisteners" +)] +#[serde(rename_all = "camelCase")] +pub struct PodListenersSpec { + /// All Listeners currently bound by the Pod. + /// + /// Indexed by Volume name (not PersistentVolume or PersistentVolumeClaim). + pub listeners: BTreeMap, +} + +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PodListener { + /// `Node` if this address only allows access to Pods hosted on a specific Kubernetes Node, otherwise `Cluster`. + pub scope: PodListenerScope, + /// Addresses allowing access to this Pod. + /// + /// Compared to `ingress_addresses` on the Listener status, this list is restricted to addresses that can access this Pod. + /// + /// This field is intended to be equivalent to the files mounted into the Listener volume. + pub ingress_addresses: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub enum PodListenerScope { + Node, + Cluster, +} diff --git a/crates/stackable-operator/src/crd/listener/mod.rs b/crates/stackable-operator/src/crd/listener/mod.rs new file mode 100644 index 000000000..375a85b02 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/mod.rs @@ -0,0 +1,116 @@ +//! This modules provides resource types used to interact with [listener-operator](https://docs.stackable.tech/listener-operator/stable/index.html) +//! +//! # Custom Resources +//! +//! ## [`Listener`] +//! +//! Exposes a set of pods, either internally to the cluster or to the outside world. The mechanism for how it is exposed +//! is managed by the [`ListenerClass`]. +//! +//! It can be either created manually by the application administrator (for applications that expose a single load-balanced endpoint), +//! or automatically when mounting a [listener volume](`ListenerOperatorVolumeSourceBuilder`) (for applications that expose a separate endpoint +//! per replica). +//! +//! All exposed pods *must* have a mounted [listener volume](`ListenerOperatorVolumeSourceBuilder`), regardless of whether the [`Listener`] is created automatically. +//! +//! ## [`ListenerClass`] +//! +//! Declares a policy for how [`Listener`]s are exposed to users. +//! +//! It is created by the cluster administrator. +//! +//! ## [`PodListeners`] +//! +//! Informs users and other operators about the state of all [`Listener`]s associated with a [`Pod`]. +//! +//! It is created by the Stackable Secret Operator, and always named `pod-{pod.metadata.uid}`. + +#[cfg(doc)] +use k8s_openapi::api::core::v1::{ + Node, PersistentVolume, PersistentVolumeClaim, Pod, Service, Volume, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg(doc)] +use crate::builder::pod::volume::ListenerOperatorVolumeSourceBuilder; + +mod class; +mod listeners; + +pub use class::*; +pub use listeners::*; + +/// The method used to access the services. +// +// Please note that this does not necessarily need to be restricted to the same Service types Kubernetes supports. +// Listeners currently happens to support the same set of service types as upstream Kubernetes, but we still want to +// have the freedom to add custom ones in the future (for example: Istio ingress?). +#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] +pub enum ServiceType { + /// Reserve a port on each node. + NodePort, + + /// Provision a dedicated load balancer. + LoadBalancer, + + /// Assigns an IP address from a pool of IP addresses that your cluster has reserved for that purpose. + ClusterIP, +} + +/// Service Internal Traffic Policy enables internal traffic restrictions to only route internal traffic to endpoints +/// within the node the traffic originated from. The "internal" traffic here refers to traffic originated from Pods in +/// the current cluster. This can help to reduce costs and improve performance. +/// See [Kubernetes docs](https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/). +// +// Please note that this represents a Kubernetes type, so the name of the enum variant needs to exactly match the +// Kubernetes traffic policy. +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq, strum::Display)] +pub enum KubernetesTrafficPolicy { + /// Obscures the client source IP and may cause a second hop to another node, but allows Kubernetes to spread the load between all nodes. + Cluster, + + /// Preserves the client source IP and avoid a second hop for LoadBalancer and NodePort type Services, but makes clients responsible for spreading the load. + Local, +} + +/// The type of a given address. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub enum AddressType { + /// A resolvable DNS hostname. + Hostname, + + /// A resolved IP address. + #[serde(rename = "IP")] + Ip, +} + +/// A mode for deciding the preferred [`AddressType`]. +/// +/// These can vary depending on the rest of the [`ListenerClass`]. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] +pub enum PreferredAddressType { + /// Like [`AddressType::Hostname`], but prefers [`AddressType::Ip`] for [`ServiceType::NodePort`], since their hostnames are less likely to be resolvable. + HostnameConservative, + + // Like the respective variants of AddressType. Ideally we would refer to them instead of copy/pasting, but that breaks due to upstream issues: + // - https://github.com/GREsau/schemars/issues/222 + // - https://github.com/kube-rs/kube/issues/1622 + Hostname, + #[serde(rename = "IP")] + Ip, +} + +impl PreferredAddressType { + pub fn resolve(self, listener_class: &ListenerClassSpec) -> AddressType { + match self { + PreferredAddressType::HostnameConservative => match listener_class.service_type { + ServiceType::NodePort => AddressType::Ip, + _ => AddressType::Hostname, + }, + PreferredAddressType::Hostname => AddressType::Hostname, + PreferredAddressType::Ip => AddressType::Ip, + } + } +} diff --git a/crates/stackable-operator/src/crd/mod.rs b/crates/stackable-operator/src/crd/mod.rs index c3c607056..399b5d988 100644 --- a/crates/stackable-operator/src/crd/mod.rs +++ b/crates/stackable-operator/src/crd/mod.rs @@ -4,6 +4,10 @@ use educe::Educe; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +pub mod authentication; +pub mod listener; +pub mod s3; + /// A reference to a product cluster (for example, a `ZookeeperCluster`) /// /// `namespace`'s defaulting only applies when retrieved via [`ClusterRef::namespace_relative_from`] diff --git a/crates/stackable-operator/src/crd/s3/bucket.rs b/crates/stackable-operator/src/crd/s3/bucket.rs new file mode 100644 index 000000000..4eb100d41 --- /dev/null +++ b/crates/stackable-operator/src/crd/s3/bucket.rs @@ -0,0 +1,102 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use snafu::{ResultExt as _, Snafu}; + +use crate::{ + client::Client, + crd::s3::{ConnectionError, ConnectionInlineOrReference, ConnectionSpec}, +}; + +#[derive(Debug, Snafu)] +pub enum BucketError { + #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] + RetrieveS3Connection { + source: crate::client::Error, + s3_connection: String, + }, + + #[snafu(display("failed to resolve S3 connection"))] + ResolveConnection { source: ConnectionError }, +} + +/// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. +/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). +#[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[kube( + group = "s3.stackable.tech", + version = "v1alpha1", + kind = "S3Bucket", + plural = "s3buckets", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct BucketSpec { + /// The name of the S3 bucket. + pub bucket_name: String, + + /// The definition of an S3 connection, either inline or as a reference. + pub connection: ConnectionInlineOrReference, +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +// TODO: This probably should be serde(untagged), but this would be a breaking change +pub enum BucketInlineOrReference { + Inline(BucketSpec), + Reference(String), +} + +/// Use this struct in your operator. +pub struct ResolvedBucket { + pub bucket_name: String, + pub connection: ConnectionSpec, +} + +impl BucketInlineOrReference { + pub async fn resolve( + self, + client: &Client, + namespace: &str, + ) -> Result { + match self { + Self::Inline(inline) => { + let connection = inline + .connection + .resolve(client, namespace) + .await + .context(ResolveConnectionSnafu)?; + + Ok(ResolvedBucket { + bucket_name: inline.bucket_name, + connection, + }) + } + Self::Reference(reference) => { + let bucket_spec = client + .get::(&reference, namespace) + .await + .context(RetrieveS3ConnectionSnafu { + s3_connection: reference, + })? + .spec; + + let connection = bucket_spec + .connection + .resolve(client, namespace) + .await + .context(ResolveConnectionSnafu)?; + + Ok(ResolvedBucket { + bucket_name: bucket_spec.bucket_name, + connection, + }) + } + } + } +} diff --git a/crates/stackable-operator/src/commons/s3/helpers.rs b/crates/stackable-operator/src/crd/s3/connection.rs similarity index 53% rename from crates/stackable-operator/src/commons/s3/helpers.rs rename to crates/stackable-operator/src/crd/s3/connection.rs index 1cb13eac3..160166ec1 100644 --- a/crates/stackable-operator/src/commons/s3/helpers.rs +++ b/crates/stackable-operator/src/crd/s3/connection.rs @@ -1,55 +1,216 @@ -use k8s_openapi::api::core::v1::{Volume, VolumeMount}; +use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use snafu::ResultExt; +use snafu::{ResultExt as _, Snafu}; use url::Url; use crate::{ builder::pod::{container::ContainerBuilder, volume::VolumeMountBuilder, PodBuilder}, client::Client, commons::{ - authentication::SECRET_BASE_PATH, - s3::{ - AddS3CredentialVolumesSnafu, AddS3TlsClientDetailsVolumesSnafu, AddVolumeMountsSnafu, - AddVolumesSnafu, ParseS3EndpointSnafu, RetrieveS3ConnectionSnafu, S3Bucket, - S3BucketSpec, S3Connection, S3ConnectionSpec, S3Error, SetS3EndpointSchemeSnafu, - }, + networking::HostName, + secret_class::{SecretClassVolume, SecretClassVolumeError}, + tls_verification::{TlsClientDetails, TlsClientDetailsError}, }, + constants::secret::SECRET_BASE_PATH, + k8s_openapi::api::core::v1::{Volume, VolumeMount}, }; +#[derive(Debug, Snafu)] +pub enum ConnectionError { + #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] + RetrieveS3Connection { + source: crate::client::Error, + s3_connection: String, + }, + + #[snafu(display("failed to parse S3 endpoint '{endpoint}'"))] + ParseS3Endpoint { + source: url::ParseError, + endpoint: String, + }, + + #[snafu(display("failed to set S3 endpoint scheme '{scheme}' for endpoint '{endpoint}'"))] + SetS3EndpointScheme { endpoint: Url, scheme: String }, + + #[snafu(display("failed to add S3 credential volumes and volume mounts"))] + AddS3CredentialVolumes { source: SecretClassVolumeError }, + + #[snafu(display("failed to add S3 TLS client details volumes and volume mounts"))] + AddS3TlsClientDetailsVolumes { source: TlsClientDetailsError }, + + #[snafu(display("failed to add required volumes"))] + AddVolumes { source: crate::builder::pod::Error }, + + #[snafu(display("failed to add required volumeMounts"))] + AddVolumeMounts { + source: crate::builder::pod::container::Error, + }, +} + +/// S3 connection definition as a resource. +/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). +#[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[kube( + group = "s3.stackable.tech", + version = "v1alpha1", + kind = "S3Connection", + plural = "s3connections", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionSpec { + /// Host of the S3 server without any protocol or port. For example: `west1.my-cloud.com`. + pub host: HostName, + + /// Port the S3 server listens on. + /// If not specified the product will determine the port to use. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub port: Option, + + /// AWS service API region used by the AWS SDK when using AWS S3 buckets. + /// + /// This defaults to `us-east-1` and can be ignored if not using AWS S3 + /// buckets. + /// + /// NOTE: This is not the bucket region, and is used by the AWS SDK to + /// construct endpoints for various AWS service APIs. It is only useful when + /// using AWS S3 buckets. + /// + /// When using AWS S3 buckets, you can configure optimal AWS service API + /// connections in the following ways: + /// - From **inside** AWS: Use an auto-discovery source (eg: AWS IMDS). + /// - From **outside** AWS, or when IMDS is disabled, explicity set the + /// region name nearest to where the client application is running from. + #[serde(default)] + pub region: AwsRegion, + + /// Which access style to use. + /// Defaults to virtual hosted-style as most of the data products out there. + /// Have a look at the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html). + #[serde(default)] + pub access_style: AccessStyle, + + /// If the S3 uses authentication you have to specify you S3 credentials. + /// In the most cases a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) + /// providing `accessKey` and `secretKey` is sufficient. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credentials: Option, + + /// Use a TLS connection. If not specified no TLS will be used. + #[serde(flatten)] + pub tls: TlsClientDetails, +} + +#[derive( + strum::Display, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, +)] +#[strum(serialize_all = "PascalCase")] +pub enum AccessStyle { + /// Use path-style access as described in + Path, + + /// Use as virtual hosted-style access as described in + #[default] + VirtualHosted, +} + +/// Set a named AWS region, or defer to an auto-discovery mechanism. +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum AwsRegion { + /// Defer region detection to an auto-discovery mechanism. + Source(AwsRegionAutoDiscovery), + + /// An explicit region, eg: eu-central-1 + Name(String), +} + +impl AwsRegion { + /// Get the AWS region name. + /// + /// Returns `None` if an auto-discovery source has been selected. Otherwise, + /// it returns the configured region name. + /// + /// Example usage: + /// + /// ``` + /// # use stackable_operator::commons::s3::AwsRegion; + /// # fn set_property(key: &str, value: &str) {} + /// # fn example(aws_region: AwsRegion) { + /// if let Some(region_name) = aws_region.name() { + /// // set some property if the region is set, or is the default. + /// set_property("aws.region", region_name); + /// }; + /// # } + /// ``` + pub fn name(&self) -> Option<&str> { + match self { + AwsRegion::Name(name) => Some(name), + AwsRegion::Source(_) => None, + } + } +} + +impl Default for AwsRegion { + fn default() -> Self { + Self::Name("us-east-1".to_owned()) + } +} + +/// AWS region auto-discovery mechanism. +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub enum AwsRegionAutoDiscovery { + /// AWS Instance Meta Data Service. + /// + /// This variant should result in no region being given to the AWS SDK, + /// which should, in turn, query the AWS IMDS. + AwsImds, +} + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] // TODO: This probably should be serde(untagged), but this would be a breaking change -pub enum S3ConnectionInlineOrReference { - Inline(S3ConnectionSpec), +pub enum ConnectionInlineOrReference { + Inline(ConnectionSpec), Reference(String), } /// Use this type in you operator! -pub type ResolvedS3Connection = S3ConnectionSpec; +pub type ResolvedConnection = ConnectionSpec; -impl S3ConnectionInlineOrReference { +impl ConnectionInlineOrReference { pub async fn resolve( self, client: &Client, namespace: &str, - ) -> Result { + ) -> Result { match self { Self::Inline(inline) => Ok(inline), - Self::Reference(reference) => Ok(client - .get::(&reference, namespace) - .await - .context(RetrieveS3ConnectionSnafu { - s3_connection: reference, - })? - .spec), + Self::Reference(reference) => { + let connection_spec = client + .get::(&reference, namespace) + .await + .context(RetrieveS3ConnectionSnafu { + s3_connection: reference, + })? + .spec; + + Ok(connection_spec) + } } } } -impl ResolvedS3Connection { +impl ResolvedConnection { /// Build the endpoint URL from this connection - pub fn endpoint(&self) -> Result { + pub fn endpoint(&self) -> Result { let endpoint = format!( "http://{host}:{port}", host = self.host.as_url_host(), @@ -84,7 +245,7 @@ impl ResolvedS3Connection { &self, pod_builder: &mut PodBuilder, container_builders: Vec<&mut ContainerBuilder>, - ) -> Result<(), S3Error> { + ) -> Result<(), ConnectionError> { let (volumes, mounts) = self.volumes_and_mounts()?; pod_builder.add_volumes(volumes).context(AddVolumesSnafu)?; for cb in container_builders { @@ -97,7 +258,7 @@ impl ResolvedS3Connection { /// It is recommended to use [`Self::add_volumes_and_mounts`], this function returns you the /// volumes and mounts in case you need to add them by yourself. - pub fn volumes_and_mounts(&self) -> Result<(Vec, Vec), S3Error> { + pub fn volumes_and_mounts(&self) -> Result<(Vec, Vec), ConnectionError> { let mut volumes = Vec::new(); let mut mounts = Vec::new(); @@ -140,48 +301,6 @@ impl ResolvedS3Connection { } } -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -// TODO: This probably should be serde(untagged), but this would be a breaking change -pub enum S3BucketInlineOrReference { - Inline(S3BucketSpec), - Reference(String), -} - -/// Use this struct in your operator. -pub struct ResolvedS3Bucket { - pub bucket_name: String, - pub connection: S3ConnectionSpec, -} - -impl S3BucketInlineOrReference { - pub async fn resolve( - self, - client: &Client, - namespace: &str, - ) -> Result { - match self { - Self::Inline(inline) => Ok(ResolvedS3Bucket { - bucket_name: inline.bucket_name, - connection: inline.connection.resolve(client, namespace).await?, - }), - Self::Reference(reference) => { - let bucket = client - .get::(&reference, namespace) - .await - .context(RetrieveS3ConnectionSnafu { - s3_connection: reference, - })? - .spec; - Ok(ResolvedS3Bucket { - bucket_name: bucket.bucket_name, - connection: bucket.connection.resolve(client, namespace).await?, - }) - } - } - } -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -195,7 +314,7 @@ mod tests { // We cant test the correct resolve, as we can't mock the k8s API. #[test] fn http_endpoint() { - let s3 = ResolvedS3Connection { + let s3 = ResolvedConnection { host: "minio".parse().unwrap(), port: None, access_style: Default::default(), @@ -212,7 +331,7 @@ mod tests { #[test] fn https_endpoint() { - let s3 = ResolvedS3Connection { + let s3 = ResolvedConnection { host: "s3-eu-central-2.ionoscloud.com".parse().unwrap(), port: None, access_style: Default::default(), @@ -270,7 +389,7 @@ mod tests { #[test] fn https_without_verification() { - let s3 = ResolvedS3Connection { + let s3 = ResolvedConnection { host: "minio".parse().unwrap(), port: Some(1234), access_style: Default::default(), diff --git a/crates/stackable-operator/src/crd/s3/mod.rs b/crates/stackable-operator/src/crd/s3/mod.rs new file mode 100644 index 000000000..deab9a291 --- /dev/null +++ b/crates/stackable-operator/src/crd/s3/mod.rs @@ -0,0 +1,5 @@ +mod bucket; +mod connection; + +pub use bucket::*; +pub use connection::*; diff --git a/crates/stackable-operator/src/lib.rs b/crates/stackable-operator/src/lib.rs index 50567fb88..c4dfbcff2 100644 --- a/crates/stackable-operator/src/lib.rs +++ b/crates/stackable-operator/src/lib.rs @@ -4,6 +4,7 @@ pub mod client; pub mod cluster_resources; pub mod commons; pub mod config; +pub mod constants; pub mod cpu; pub mod crd; pub mod helm; From 8620056a89abddd22208de3ac232bfcb1930f3fe Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 26 Feb 2025 14:22:52 +0100 Subject: [PATCH 04/22] docs: Adjust and fix doc comments --- .../src/crd/listener/class.rs | 2 ++ .../src/crd/listener/mod.rs | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/stackable-operator/src/crd/listener/class.rs b/crates/stackable-operator/src/crd/listener/class.rs index 196ee4a78..e5414dac1 100644 --- a/crates/stackable-operator/src/crd/listener/class.rs +++ b/crates/stackable-operator/src/crd/listener/class.rs @@ -1,5 +1,7 @@ use std::collections::BTreeMap; +#[cfg(doc)] +use k8s_openapi::api::core::v1::Service; use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/stackable-operator/src/crd/listener/mod.rs b/crates/stackable-operator/src/crd/listener/mod.rs index 375a85b02..55ff3419b 100644 --- a/crates/stackable-operator/src/crd/listener/mod.rs +++ b/crates/stackable-operator/src/crd/listener/mod.rs @@ -1,17 +1,18 @@ -//! This modules provides resource types used to interact with [listener-operator](https://docs.stackable.tech/listener-operator/stable/index.html) +//! This modules provides resource types used to interact with [listener-operator][listener-docs]. //! //! # Custom Resources //! //! ## [`Listener`] //! -//! Exposes a set of pods, either internally to the cluster or to the outside world. The mechanism for how it is exposed -//! is managed by the [`ListenerClass`]. +//! Exposes a set of pods, either internally to the cluster or to the outside world. The mechanism +//! for how it is exposed is managed by the [`ListenerClass`]. //! -//! It can be either created manually by the application administrator (for applications that expose a single load-balanced endpoint), -//! or automatically when mounting a [listener volume](`ListenerOperatorVolumeSourceBuilder`) (for applications that expose a separate endpoint -//! per replica). +//! It can be either created manually by the application administrator (for applications that expose +//! a single load-balanced endpoint), or automatically when mounting a [listener volume][lvb] (for +//! applications that expose a separate endpoint per replica). //! -//! All exposed pods *must* have a mounted [listener volume](`ListenerOperatorVolumeSourceBuilder`), regardless of whether the [`Listener`] is created automatically. +//! All exposed pods *must* have a mounted [listener volume][lvb], regardless of whether the +//! [`Listener`] is created automatically. //! //! ## [`ListenerClass`] //! @@ -24,11 +25,12 @@ //! Informs users and other operators about the state of all [`Listener`]s associated with a [`Pod`]. //! //! It is created by the Stackable Secret Operator, and always named `pod-{pod.metadata.uid}`. +//! +//! [listener-docs]: https://docs.stackable.tech/listener-operator/stable/index.html +//! [lvb]: ListenerOperatorVolumeSourceBuilder #[cfg(doc)] -use k8s_openapi::api::core::v1::{ - Node, PersistentVolume, PersistentVolumeClaim, Pod, Service, Volume, -}; +use k8s_openapi::api::core::v1::{Node, PersistentVolume, PersistentVolumeClaim, Pod, Volume}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; From 03caeceed74632beadc7d281f774c2c21bbd08ac Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 12:23:09 +0100 Subject: [PATCH 05/22] chore: Version S3 CRDs --- .../stackable-operator/src/crd/s3/bucket.rs | 93 +++++----- .../src/crd/s3/connection.rs | 163 +++++++++--------- crates/stackable-operator/src/crd/s3/mod.rs | 8 +- 3 files changed, 140 insertions(+), 124 deletions(-) diff --git a/crates/stackable-operator/src/crd/s3/bucket.rs b/crates/stackable-operator/src/crd/s3/bucket.rs index 4eb100d41..5d0f74c4c 100644 --- a/crates/stackable-operator/src/crd/s3/bucket.rs +++ b/crates/stackable-operator/src/crd/s3/bucket.rs @@ -2,11 +2,9 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use snafu::{ResultExt as _, Snafu}; +use stackable_versioned::versioned; -use crate::{ - client::Client, - crd::s3::{ConnectionError, ConnectionInlineOrReference, ConnectionSpec}, -}; +use crate::{client::Client, crd::s3::ConnectionError}; #[derive(Debug, Snafu)] pub enum BucketError { @@ -20,50 +18,61 @@ pub enum BucketError { ResolveConnection { source: ConnectionError }, } -/// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. -/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). -#[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[kube( - group = "s3.stackable.tech", - version = "v1alpha1", - kind = "S3Bucket", - plural = "s3buckets", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced -)] -#[serde(rename_all = "camelCase")] -pub struct BucketSpec { - /// The name of the S3 bucket. - pub bucket_name: String, +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + // This makes it possible to refer to S3 connection related structs and enums + // by v1alpha1. + // NOTE (@Techassi): However, this will break once items defined in here will + // reference each other by v1alpha1. One possible solution is to import + // connection::v1alpha1 as v1alpha1_conn or similar. + mod v1alpha1 { + use crate::crd::s3::connection::v1alpha1; + } - /// The definition of an S3 connection, either inline or as a reference. - pub connection: ConnectionInlineOrReference, -} + /// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. + /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). + #[versioned(k8s( + group = "s3.stackable.tech", + kind = "S3Bucket", + plural = "s3buckets", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced + ))] + #[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct BucketSpec { + /// The name of the S3 bucket. + pub bucket_name: String, -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -// TODO: This probably should be serde(untagged), but this would be a breaking change -pub enum BucketInlineOrReference { - Inline(BucketSpec), - Reference(String), -} + /// The definition of an S3 connection, either inline or as a reference. + pub connection: v1alpha1::InlineConnectionOrReference, + } -/// Use this struct in your operator. -pub struct ResolvedBucket { - pub bucket_name: String, - pub connection: ConnectionSpec, + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + // TODO: This probably should be serde(untagged), but this would be a breaking change + pub enum InlineBucketOrReference { + Inline(BucketSpec), + Reference(String), + } + + /// Use this struct in your operator. + pub struct ResolvedBucket { + pub bucket_name: String, + pub connection: v1alpha1::ConnectionSpec, + } } -impl BucketInlineOrReference { +impl v1alpha1::InlineBucketOrReference { pub async fn resolve( self, client: &Client, namespace: &str, - ) -> Result { + ) -> Result { match self { Self::Inline(inline) => { let connection = inline @@ -72,14 +81,14 @@ impl BucketInlineOrReference { .await .context(ResolveConnectionSnafu)?; - Ok(ResolvedBucket { + Ok(v1alpha1::ResolvedBucket { bucket_name: inline.bucket_name, connection, }) } Self::Reference(reference) => { let bucket_spec = client - .get::(&reference, namespace) + .get::(&reference, namespace) .await .context(RetrieveS3ConnectionSnafu { s3_connection: reference, @@ -92,7 +101,7 @@ impl BucketInlineOrReference { .await .context(ResolveConnectionSnafu)?; - Ok(ResolvedBucket { + Ok(v1alpha1::ResolvedBucket { bucket_name: bucket_spec.bucket_name, connection, }) diff --git a/crates/stackable-operator/src/crd/s3/connection.rs b/crates/stackable-operator/src/crd/s3/connection.rs index 71c29840f..fd116e173 100644 --- a/crates/stackable-operator/src/crd/s3/connection.rs +++ b/crates/stackable-operator/src/crd/s3/connection.rs @@ -2,6 +2,7 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use snafu::{ResultExt as _, Snafu}; +use stackable_versioned::versioned; use url::Url; use crate::{ @@ -48,78 +49,88 @@ pub enum ConnectionError { }, } -/// S3 connection definition as a resource. -/// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). -#[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[kube( - group = "s3.stackable.tech", - version = "v1alpha1", - kind = "S3Connection", - plural = "s3connections", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced -)] -#[serde(rename_all = "camelCase")] -pub struct ConnectionSpec { - /// Host of the S3 server without any protocol or port. For example: `west1.my-cloud.com`. - pub host: HostName, - - /// Port the S3 server listens on. - /// If not specified the product will determine the port to use. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub port: Option, - - /// Bucket region used for signing headers (sigv4). - /// - /// This defaults to `us-east-1` which is compatible with other implementations such as Minio. - /// - /// WARNING: Some products use the Hadoop S3 implementation which falls back to us-east-2. - #[serde(default)] - pub region: Region, - - /// Which access style to use. - /// Defaults to virtual hosted-style as most of the data products out there. - /// Have a look at the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html). - #[serde(default)] - pub access_style: S3AccessStyle, - - /// If the S3 uses authentication you have to specify you S3 credentials. - /// In the most cases a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) - /// providing `accessKey` and `secretKey` is sufficient. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub credentials: Option, - - /// Use a TLS connection. If not specified no TLS will be used. - #[serde(flatten)] - pub tls: TlsClientDetails, -} +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// S3 connection definition as a resource. + /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). + #[versioned(k8s( + group = "s3.stackable.tech", + kind = "S3Connection", + plural = "s3connections", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced + ))] + #[derive(CustomResource, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ConnectionSpec { + /// Host of the S3 server without any protocol or port. For example: `west1.my-cloud.com`. + pub host: HostName, + + /// Port the S3 server listens on. + /// If not specified the product will determine the port to use. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub port: Option, + + /// Bucket region used for signing headers (sigv4). + /// + /// This defaults to `us-east-1` which is compatible with other implementations such as Minio. + /// + /// WARNING: Some products use the Hadoop S3 implementation which falls back to us-east-2. + #[serde(default)] + pub region: Region, + + /// Which access style to use. + /// Defaults to virtual hosted-style as most of the data products out there. + /// Have a look at the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html). + #[serde(default)] + pub access_style: S3AccessStyle, + + /// If the S3 uses authentication you have to specify you S3 credentials. + /// In the most cases a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) + /// providing `accessKey` and `secretKey` is sufficient. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credentials: Option, + + /// Use a TLS connection. If not specified no TLS will be used. + #[serde(flatten)] + pub tls: TlsClientDetails, + } -#[derive( - strum::Display, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, -)] -#[strum(serialize_all = "PascalCase")] -pub enum S3AccessStyle { - /// Use path-style access as described in - Path, - - /// Use as virtual hosted-style access as described in - #[default] - VirtualHosted, -} + #[derive( + strum::Display, Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize, + )] + #[strum(serialize_all = "PascalCase")] + pub enum S3AccessStyle { + /// Use path-style access as described in + Path, + + /// Use as virtual hosted-style access as described in + #[default] + VirtualHosted, + } -/// Set a named S3 Bucket region. -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Region { - #[serde(default = "Region::default_region_name")] - pub name: String, + /// Set a named S3 Bucket region. + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Region { + #[serde(default = "v1alpha1::Region::default_region_name")] + pub name: String, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + // TODO: This probably should be serde(untagged), but this would be a breaking change + pub enum InlineConnectionOrReference { + Inline(ConnectionSpec), + Reference(String), + } } -impl Region { +impl v1alpha1::Region { /// Having it as `const &str` as well, so we don't always allocate a [`String`] just for comparisons pub const DEFAULT_REGION_NAME: &str = "us-east-1"; @@ -137,7 +148,7 @@ impl Region { } } -impl Default for Region { +impl Default for v1alpha1::Region { fn default() -> Self { Self { name: Self::default_region_name(), @@ -145,18 +156,10 @@ impl Default for Region { } } -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -// TODO: This probably should be serde(untagged), but this would be a breaking change -pub enum ConnectionInlineOrReference { - Inline(ConnectionSpec), - Reference(String), -} - /// Use this type in you operator! -pub type ResolvedConnection = ConnectionSpec; +pub type ResolvedConnection = v1alpha1::ConnectionSpec; -impl ConnectionInlineOrReference { +impl v1alpha1::InlineConnectionOrReference { pub async fn resolve( self, client: &Client, @@ -166,7 +169,7 @@ impl ConnectionInlineOrReference { Self::Inline(inline) => Ok(inline), Self::Reference(reference) => { let connection_spec = client - .get::(&reference, namespace) + .get::(&reference, namespace) .await .context(RetrieveS3ConnectionSnafu { s3_connection: reference, diff --git a/crates/stackable-operator/src/crd/s3/mod.rs b/crates/stackable-operator/src/crd/s3/mod.rs index deab9a291..72913da82 100644 --- a/crates/stackable-operator/src/crd/s3/mod.rs +++ b/crates/stackable-operator/src/crd/s3/mod.rs @@ -1,5 +1,9 @@ mod bucket; mod connection; -pub use bucket::*; -pub use connection::*; +pub use connection::ConnectionError; + +// Group all v1alpha1 items in one module +pub mod v1alpha1 { + pub use super::{bucket::v1alpha1::*, connection::v1alpha1::*}; +} From 9d1219f3bd6d06498699464221ebc86ab85d4ac8 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 14:08:24 +0100 Subject: [PATCH 06/22] chore: Split S3 types and impl blocks --- .../stackable-operator/src/crd/s3/bucket.rs | 111 ------------------ .../src/crd/s3/bucket/mod.rs | 64 ++++++++++ .../src/crd/s3/bucket/v1alpha1_impl.rs | 54 +++++++++ .../s3/{connection.rs => connection/mod.rs} | 57 ++------- .../src/crd/s3/connection/v1alpha1_impl.rs | 58 +++++++++ crates/stackable-operator/src/crd/s3/mod.rs | 4 +- 6 files changed, 186 insertions(+), 162 deletions(-) delete mode 100644 crates/stackable-operator/src/crd/s3/bucket.rs create mode 100644 crates/stackable-operator/src/crd/s3/bucket/mod.rs create mode 100644 crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs rename crates/stackable-operator/src/crd/s3/{connection.rs => connection/mod.rs} (88%) create mode 100644 crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs diff --git a/crates/stackable-operator/src/crd/s3/bucket.rs b/crates/stackable-operator/src/crd/s3/bucket.rs deleted file mode 100644 index 5d0f74c4c..000000000 --- a/crates/stackable-operator/src/crd/s3/bucket.rs +++ /dev/null @@ -1,111 +0,0 @@ -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use snafu::{ResultExt as _, Snafu}; -use stackable_versioned::versioned; - -use crate::{client::Client, crd::s3::ConnectionError}; - -#[derive(Debug, Snafu)] -pub enum BucketError { - #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] - RetrieveS3Connection { - source: crate::client::Error, - s3_connection: String, - }, - - #[snafu(display("failed to resolve S3 connection"))] - ResolveConnection { source: ConnectionError }, -} - -#[versioned(version(name = "v1alpha1"))] -pub mod versioned { - // This makes it possible to refer to S3 connection related structs and enums - // by v1alpha1. - // NOTE (@Techassi): However, this will break once items defined in here will - // reference each other by v1alpha1. One possible solution is to import - // connection::v1alpha1 as v1alpha1_conn or similar. - mod v1alpha1 { - use crate::crd::s3::connection::v1alpha1; - } - - /// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. - /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). - #[versioned(k8s( - group = "s3.stackable.tech", - kind = "S3Bucket", - plural = "s3buckets", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ), - namespaced - ))] - #[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] - #[serde(rename_all = "camelCase")] - pub struct BucketSpec { - /// The name of the S3 bucket. - pub bucket_name: String, - - /// The definition of an S3 connection, either inline or as a reference. - pub connection: v1alpha1::InlineConnectionOrReference, - } - - #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] - #[serde(rename_all = "camelCase")] - // TODO: This probably should be serde(untagged), but this would be a breaking change - pub enum InlineBucketOrReference { - Inline(BucketSpec), - Reference(String), - } - - /// Use this struct in your operator. - pub struct ResolvedBucket { - pub bucket_name: String, - pub connection: v1alpha1::ConnectionSpec, - } -} - -impl v1alpha1::InlineBucketOrReference { - pub async fn resolve( - self, - client: &Client, - namespace: &str, - ) -> Result { - match self { - Self::Inline(inline) => { - let connection = inline - .connection - .resolve(client, namespace) - .await - .context(ResolveConnectionSnafu)?; - - Ok(v1alpha1::ResolvedBucket { - bucket_name: inline.bucket_name, - connection, - }) - } - Self::Reference(reference) => { - let bucket_spec = client - .get::(&reference, namespace) - .await - .context(RetrieveS3ConnectionSnafu { - s3_connection: reference, - })? - .spec; - - let connection = bucket_spec - .connection - .resolve(client, namespace) - .await - .context(ResolveConnectionSnafu)?; - - Ok(v1alpha1::ResolvedBucket { - bucket_name: bucket_spec.bucket_name, - connection, - }) - } - } - } -} diff --git a/crates/stackable-operator/src/crd/s3/bucket/mod.rs b/crates/stackable-operator/src/crd/s3/bucket/mod.rs new file mode 100644 index 000000000..f734b0de0 --- /dev/null +++ b/crates/stackable-operator/src/crd/s3/bucket/mod.rs @@ -0,0 +1,64 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use snafu::Snafu; +use stackable_versioned::versioned; + +use crate::crd::s3::{connection::v1alpha1 as conn_v1alpha1, ConnectionError}; + +mod v1alpha1_impl; + +// NOTE (@Techassi): Where should this error be placed? Technically errors can +// change between version., because version-specific impl blocks might need +// different variants or might use a completely different error type. +#[derive(Debug, Snafu)] +pub enum BucketError { + #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] + RetrieveS3Connection { + source: crate::client::Error, + s3_connection: String, + }, + + #[snafu(display("failed to resolve S3 connection"))] + ResolveConnection { source: ConnectionError }, +} + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// S3 bucket specification containing the bucket name and an inlined or referenced connection specification. + /// Learn more on the [S3 concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/s3). + #[versioned(k8s( + group = "s3.stackable.tech", + kind = "S3Bucket", + plural = "s3buckets", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ), + namespaced + ))] + #[derive(Clone, CustomResource, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct BucketSpec { + /// The name of the S3 bucket. + pub bucket_name: String, + + /// The definition of an S3 connection, either inline or as a reference. + pub connection: conn_v1alpha1::InlineConnectionOrReference, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + // TODO: This probably should be serde(untagged), but this would be a breaking change + pub enum InlineBucketOrReference { + Inline(BucketSpec), + Reference(String), + } + + /// Use this struct in your operator. + pub struct ResolvedBucket { + pub bucket_name: String, + pub connection: conn_v1alpha1::ConnectionSpec, + } +} diff --git a/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs new file mode 100644 index 000000000..3a003959e --- /dev/null +++ b/crates/stackable-operator/src/crd/s3/bucket/v1alpha1_impl.rs @@ -0,0 +1,54 @@ +//! v1alpha1 specific implementations for S3 buckets. + +use snafu::ResultExt as _; + +use crate::{ + client::Client, + crd::s3::bucket::{ + v1alpha1::{InlineBucketOrReference, ResolvedBucket, S3Bucket}, + BucketError, ResolveConnectionSnafu, RetrieveS3ConnectionSnafu, + }, +}; + +impl InlineBucketOrReference { + pub async fn resolve( + self, + client: &Client, + namespace: &str, + ) -> Result { + match self { + Self::Inline(inline) => { + let connection = inline + .connection + .resolve(client, namespace) + .await + .context(ResolveConnectionSnafu)?; + + Ok(ResolvedBucket { + bucket_name: inline.bucket_name, + connection, + }) + } + Self::Reference(reference) => { + let bucket_spec = client + .get::(&reference, namespace) + .await + .context(RetrieveS3ConnectionSnafu { + s3_connection: reference, + })? + .spec; + + let connection = bucket_spec + .connection + .resolve(client, namespace) + .await + .context(ResolveConnectionSnafu)?; + + Ok(ResolvedBucket { + bucket_name: bucket_spec.bucket_name, + connection, + }) + } + } + } +} diff --git a/crates/stackable-operator/src/crd/s3/connection.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs similarity index 88% rename from crates/stackable-operator/src/crd/s3/connection.rs rename to crates/stackable-operator/src/crd/s3/connection/mod.rs index fd116e173..c3849a93a 100644 --- a/crates/stackable-operator/src/crd/s3/connection.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -7,7 +7,6 @@ use url::Url; use crate::{ builder::pod::{container::ContainerBuilder, volume::VolumeMountBuilder, PodBuilder}, - client::Client, commons::{ networking::HostName, secret_class::{SecretClassVolume, SecretClassVolumeError}, @@ -17,6 +16,11 @@ use crate::{ k8s_openapi::api::core::v1::{Volume, VolumeMount}, }; +mod v1alpha1_impl; + +// NOTE (@Techassi): Where should this error be placed? Technically errors can +// change between version., because version-specific impl blocks might need +// different variants or might use a completely different error type. #[derive(Debug, Snafu)] pub enum ConnectionError { #[snafu(display("failed to retrieve S3 connection '{s3_connection}'"))] @@ -130,58 +134,11 @@ pub mod versioned { } } -impl v1alpha1::Region { - /// Having it as `const &str` as well, so we don't always allocate a [`String`] just for comparisons - pub const DEFAULT_REGION_NAME: &str = "us-east-1"; - - fn default_region_name() -> String { - Self::DEFAULT_REGION_NAME.to_string() - } - - /// Returns if the region sticks to the Stackable defaults. - /// - /// Some products don't really support configuring the region. - /// This function can be used to determine if a warning or error should be raised to inform the - /// user of this situation. - pub fn is_default_config(&self) -> bool { - self.name == Self::DEFAULT_REGION_NAME - } -} - -impl Default for v1alpha1::Region { - fn default() -> Self { - Self { - name: Self::default_region_name(), - } - } -} - +// FIXME (@Techassi): This should be versioned as well, but the macro cannot +// handle new-type structs yet. /// Use this type in you operator! pub type ResolvedConnection = v1alpha1::ConnectionSpec; -impl v1alpha1::InlineConnectionOrReference { - pub async fn resolve( - self, - client: &Client, - namespace: &str, - ) -> Result { - match self { - Self::Inline(inline) => Ok(inline), - Self::Reference(reference) => { - let connection_spec = client - .get::(&reference, namespace) - .await - .context(RetrieveS3ConnectionSnafu { - s3_connection: reference, - })? - .spec; - - Ok(connection_spec) - } - } - } -} - impl ResolvedConnection { /// Build the endpoint URL from this connection pub fn endpoint(&self) -> Result { diff --git a/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs new file mode 100644 index 000000000..b446d6601 --- /dev/null +++ b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs @@ -0,0 +1,58 @@ +use snafu::ResultExt as _; + +use crate::{ + client::Client, + crd::s3::{ + connection::{ConnectionError, ResolvedConnection, RetrieveS3ConnectionSnafu}, + v1alpha1::{InlineConnectionOrReference, Region, S3Connection}, + }, +}; + +impl Region { + /// Having it as `const &str` as well, so we don't always allocate a [`String`] just for comparisons + pub const DEFAULT_REGION_NAME: &str = "us-east-1"; + + pub(super) fn default_region_name() -> String { + Self::DEFAULT_REGION_NAME.to_string() + } + + /// Returns if the region sticks to the Stackable defaults. + /// + /// Some products don't really support configuring the region. + /// This function can be used to determine if a warning or error should be raised to inform the + /// user of this situation. + pub fn is_default_config(&self) -> bool { + self.name == Self::DEFAULT_REGION_NAME + } +} + +impl Default for Region { + fn default() -> Self { + Self { + name: Self::default_region_name(), + } + } +} + +impl InlineConnectionOrReference { + pub async fn resolve( + self, + client: &Client, + namespace: &str, + ) -> Result { + match self { + Self::Inline(inline) => Ok(inline), + Self::Reference(reference) => { + let connection_spec = client + .get::(&reference, namespace) + .await + .context(RetrieveS3ConnectionSnafu { + s3_connection: reference, + })? + .spec; + + Ok(connection_spec) + } + } + } +} diff --git a/crates/stackable-operator/src/crd/s3/mod.rs b/crates/stackable-operator/src/crd/s3/mod.rs index 72913da82..625a10621 100644 --- a/crates/stackable-operator/src/crd/s3/mod.rs +++ b/crates/stackable-operator/src/crd/s3/mod.rs @@ -1,9 +1,11 @@ mod bucket; mod connection; +// Publicly re-export unversioned items, in this case errors. +pub use bucket::BucketError; pub use connection::ConnectionError; -// Group all v1alpha1 items in one module +// Group all v1alpha1 items in one module. pub mod v1alpha1 { pub use super::{bucket::v1alpha1::*, connection::v1alpha1::*}; } From 88a5b3099dff78ebb3c5dfb3c35b2d296dbe967e Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 14:09:05 +0100 Subject: [PATCH 07/22] test: Fix AuthenticationClass doc test --- crates/stackable-operator/src/crd/authentication/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/crd/authentication/mod.rs b/crates/stackable-operator/src/crd/authentication/mod.rs index d9a69ee1f..829973572 100644 --- a/crates/stackable-operator/src/crd/authentication/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/mod.rs @@ -112,7 +112,7 @@ impl AuthenticationClass { /// ``` /// # use schemars::JsonSchema; /// # use serde::{Deserialize, Serialize}; -/// use stackable_operator::commons::authentication::ClientAuthenticationDetails; +/// use stackable_operator::crd::authentication::ClientAuthenticationDetails; /// /// #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] /// #[serde(rename_all = "camelCase")] From 29cd3726aa396a361635f5f9bb2ed9ab44f08fce Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:15:58 +0100 Subject: [PATCH 08/22] chore: Version Kerberos auth provider --- .../src/crd/authentication/{kerberos.rs => kerberos/mod.rs} | 2 ++ 1 file changed, 2 insertions(+) rename crates/stackable-operator/src/crd/authentication/{kerberos.rs => kerberos/mod.rs} (81%) diff --git a/crates/stackable-operator/src/crd/authentication/kerberos.rs b/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs similarity index 81% rename from crates/stackable-operator/src/crd/authentication/kerberos.rs rename to crates/stackable-operator/src/crd/authentication/kerberos/mod.rs index 5b4ffe893..afff0b374 100644 --- a/crates/stackable-operator/src/crd/authentication/kerberos.rs +++ b/crates/stackable-operator/src/crd/authentication/kerberos/mod.rs @@ -1,6 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; +#[versioned(version(name = "v1alpha1"))] #[derive( Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, )] From 84b31c79df9f6f1183f6858f2f07af7a34b8c5f8 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:16:19 +0100 Subject: [PATCH 09/22] chore: Version LDAP auth provider --- .../src/crd/authentication/ldap/mod.rs | 69 +++++++++++++++++ .../{ldap.rs => ldap/v1alpha1_impl.rs} | 75 +++---------------- 2 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 crates/stackable-operator/src/crd/authentication/ldap/mod.rs rename crates/stackable-operator/src/crd/authentication/{ldap.rs => ldap/v1alpha1_impl.rs} (79%) diff --git a/crates/stackable-operator/src/crd/authentication/ldap/mod.rs b/crates/stackable-operator/src/crd/authentication/ldap/mod.rs new file mode 100644 index 000000000..114196a7c --- /dev/null +++ b/crates/stackable-operator/src/crd/authentication/ldap/mod.rs @@ -0,0 +1,69 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; + +use crate::commons::{ + networking::HostName, secret_class::SecretClassVolume, tls_verification::TlsClientDetails, +}; + +mod v1alpha1_impl; + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// Host of the LDAP server, for example: `my.ldap.server` or `127.0.0.1`. + pub hostname: HostName, + + /// Port of the LDAP server. If TLS is used defaults to 636 otherwise to 389. + port: Option, + + /// LDAP search base, for example: `ou=users,dc=example,dc=org`. + #[serde(default)] + pub search_base: String, + + /// LDAP query to filter users, for example: `(memberOf=cn=myTeam,ou=teams,dc=example,dc=org)`. + #[serde(default)] + pub search_filter: String, + + /// The name of the LDAP object fields. + #[serde(default)] + pub ldap_field_names: FieldNames, + + /// In case you need a special account for searching the LDAP server you can specify it here. + bind_credentials: Option, + + /// Use a TLS connection. If not specified no TLS will be used. + #[serde(flatten)] + pub tls: TlsClientDetails, + } + + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct FieldNames { + /// The name of the username field + #[serde(default = "FieldNames::default_uid")] + pub uid: String, + + /// The name of the group field + #[serde(default = "FieldNames::default_group")] + pub group: String, + + /// The name of the firstname field + #[serde(default = "FieldNames::default_given_name")] + pub given_name: String, + + /// The name of the lastname field + #[serde(default = "FieldNames::default_surname")] + pub surname: String, + + /// The name of the email field + #[serde(default = "FieldNames::default_email")] + pub email: String, + } +} diff --git a/crates/stackable-operator/src/crd/authentication/ldap.rs b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs similarity index 79% rename from crates/stackable-operator/src/crd/authentication/ldap.rs rename to crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs index 059548078..0700b1b71 100644 --- a/crates/stackable-operator/src/crd/authentication/ldap.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs @@ -1,7 +1,5 @@ use k8s_openapi::api::core::v1::{Volume, VolumeMount}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use snafu::{ResultExt, Snafu}; +use snafu::{ResultExt as _, Snafu}; use url::{ParseError, Url}; use crate::{ @@ -9,12 +7,9 @@ use crate::{ self, pod::{container::ContainerBuilder, volume::VolumeMountBuilder, PodBuilder}, }, - commons::{ - networking::HostName, - secret_class::{SecretClassVolume, SecretClassVolumeError}, - tls_verification::{TlsClientDetails, TlsClientDetailsError}, - }, + commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError}, constants::secret::SECRET_BASE_PATH, + crd::authentication::ldap::v1alpha1::{AuthenticationProvider, FieldNames}, }; pub type Result = std::result::Result; @@ -41,37 +36,6 @@ pub enum Error { }, } -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// Host of the LDAP server, for example: `my.ldap.server` or `127.0.0.1`. - pub hostname: HostName, - - /// Port of the LDAP server. If TLS is used defaults to 636 otherwise to 389. - port: Option, - - /// LDAP search base, for example: `ou=users,dc=example,dc=org`. - #[serde(default)] - pub search_base: String, - - /// LDAP query to filter users, for example: `(memberOf=cn=myTeam,ou=teams,dc=example,dc=org)`. - #[serde(default)] - pub search_filter: String, - - /// The name of the LDAP object fields. - #[serde(default)] - pub ldap_field_names: FieldNames, - - /// In case you need a special account for searching the LDAP server you can specify it here. - bind_credentials: Option, - - /// Use a TLS connection. If not specified no TLS will be used. - #[serde(flatten)] - pub tls: TlsClientDetails, -} - impl AuthenticationProvider { /// Returns the LDAP endpoint [`Url`]. pub fn endpoint_url(&self) -> Result { @@ -168,46 +132,24 @@ impl AuthenticationProvider { } } -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct FieldNames { - /// The name of the username field - #[serde(default = "FieldNames::default_uid")] - pub uid: String, - /// The name of the group field - #[serde(default = "FieldNames::default_group")] - pub group: String, - /// The name of the firstname field - #[serde(default = "FieldNames::default_given_name")] - pub given_name: String, - /// The name of the lastname field - #[serde(default = "FieldNames::default_surname")] - pub surname: String, - /// The name of the email field - #[serde(default = "FieldNames::default_email")] - pub email: String, -} - impl FieldNames { - fn default_uid() -> String { + pub(super) fn default_uid() -> String { "uid".to_string() } - fn default_group() -> String { + pub(super) fn default_group() -> String { "memberof".to_string() } - fn default_given_name() -> String { + pub(super) fn default_given_name() -> String { "givenName".to_string() } - fn default_surname() -> String { + pub(super) fn default_surname() -> String { "sn".to_string() } - fn default_email() -> String { + pub(super) fn default_email() -> String { "mail".to_string() } } @@ -227,6 +169,7 @@ impl Default for FieldNames { #[cfg(test)] mod tests { use super::*; + use crate::commons::secret_class::SecretClassVolume; #[test] fn minimal() { From c37433d68a3b9774b83ec8c0c549e43e90667953 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:16:46 +0100 Subject: [PATCH 10/22] chore: Version OIDC auth provider --- .../src/crd/authentication/oidc/mod.rs | 107 +++++++++++++++++ .../{oidc.rs => oidc/v1alpha1_impl.rs} | 111 ++---------------- 2 files changed, 116 insertions(+), 102 deletions(-) create mode 100644 crates/stackable-operator/src/crd/authentication/oidc/mod.rs rename crates/stackable-operator/src/crd/authentication/{oidc.rs => oidc/v1alpha1_impl.rs} (74%) diff --git a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs new file mode 100644 index 000000000..e56a15c5a --- /dev/null +++ b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs @@ -0,0 +1,107 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; + +use crate::commons::{networking::HostName, tls_verification::TlsClientDetails}; +#[cfg(doc)] +use crate::crd::authentication::AuthenticationClass; + +mod v1alpha1_impl; + +// FIXME (@Techassi): These constants should also be versioned +pub const CLIENT_ID_SECRET_KEY: &str = "clientId"; +pub const CLIENT_SECRET_SECRET_KEY: &str = "clientSecret"; + +/// Do *not* use this for [`Url::join`], as the leading slash will erase the existing path! +const DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH: &str = "/.well-known/openid-configuration"; + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// This struct contains configuration values to configure an OpenID Connect + /// (OIDC) authentication class. Required fields are the identity provider + /// (IdP) `hostname` and the TLS configuration. The `port` is selected + /// automatically if not configured otherwise. The `rootPath` defaults + /// to `/`. + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// Host of the identity provider, e.g. `my.keycloak.corp` or `127.0.0.1`. + hostname: HostName, + + /// Port of the identity provider. If TLS is used defaults to 443, + /// otherwise to 80. + port: Option, + + /// Root HTTP path of the identity provider. Defaults to `/`. + #[serde(default = "v1alpha1::AuthenticationProvider::default_root_path")] + root_path: String, + + /// Use a TLS connection. If not specified no TLS will be used. + #[serde(flatten)] + pub tls: TlsClientDetails, + + /// If a product extracts some sort of "effective user" that is represented by a + /// string internally, this config determines with claim is used to extract that + /// string. It is desirable to use `sub` in here (or some other stable identifier), + /// but in many cases you might need to use `preferred_username` (e.g. in case of Keycloak) + /// or a different claim instead. + /// + /// Please note that some products hard-coded the claim in their implementation, + /// so some product operators might error out if the product hardcodes a different + /// claim than configured here. + /// + /// We don't provide any default value, as there is no correct way of doing it + /// that works in all setups. Most demos will probably use `preferred_username`, + /// although `sub` being more desirable, but technically impossible with the current + /// behavior of the products. + pub principal_claim: String, + + /// Scopes to request from your identity provider. It is recommended to + /// request the `openid`, `email`, and `profile` scopes. + pub scopes: Vec, + + /// This is a hint about which identity provider is used by the + /// AuthenticationClass. Operators *can* opt to use this + /// value to enable known quirks around OIDC / OAuth authentication. + /// Not providing a hint means there is no hint and OIDC should be used as it is + /// intended to be used (via the `.well-known` discovery). + #[serde(default)] + pub provider_hint: Option, + } + + /// An enum of supported OIDC or identity providers which can serve as a hint + /// in the product operator. Some products require special handling of + /// authentication related config options. This hint can be used to enable such + /// special handling. + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "PascalCase")] + pub enum IdentityProviderHint { + Keycloak, + } + + /// OIDC specific config options. These are set on the product config level. + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct ClientAuthenticationOptions { + /// A reference to the OIDC client credentials secret. The secret contains + /// the client id and secret. + #[serde(rename = "clientCredentialsSecret")] + pub client_credentials_secret_ref: String, + + /// An optional list of extra scopes which get merged with the scopes + /// defined in the [`AuthenticationClass`]. + #[serde(default)] + pub extra_scopes: Vec, + + // If desired, operators can add custom fields that are only needed for this specific product. + // They need to create a struct holding them and pass that as `T`. + #[serde(flatten)] + pub product_specific_fields: T, + } +} diff --git a/crates/stackable-operator/src/crd/authentication/oidc.rs b/crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs similarity index 74% rename from crates/stackable-operator/src/crd/authentication/oidc.rs rename to crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs index 8f19ee173..6766623eb 100644 --- a/crates/stackable-operator/src/crd/authentication/oidc.rs +++ b/crates/stackable-operator/src/crd/authentication/oidc/v1alpha1_impl.rs @@ -4,26 +4,21 @@ use std::{ }; use k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, SecretKeySelector}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use snafu::{ResultExt, Snafu}; +use snafu::{ResultExt as _, Snafu}; use url::{ParseError, Url}; -#[cfg(doc)] -use crate::crd::authentication::AuthenticationClass; use crate::{ commons::{networking::HostName, tls_verification::TlsClientDetails}, constants::secret::SECRET_BASE_PATH, + crd::authentication::oidc::{ + v1alpha1::{AuthenticationProvider, IdentityProviderHint}, + CLIENT_ID_SECRET_KEY, CLIENT_SECRET_SECRET_KEY, DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH, + }, }; pub type Result = std::result::Result; -pub const CLIENT_ID_SECRET_KEY: &str = "clientId"; -pub const CLIENT_SECRET_SECRET_KEY: &str = "clientSecret"; - -/// Do *not* use this for [`Url::join`], as the leading slash will erase the existing path! -const DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH: &str = "/.well-known/openid-configuration"; - +// TODO (@Techassi): Move this into mod.rs #[derive(Debug, PartialEq, Snafu)] pub enum Error { #[snafu(display("failed to parse OIDC endpoint url"))] @@ -35,64 +30,6 @@ pub enum Error { SetOidcEndpointScheme { endpoint: Url, scheme: String }, } -/// This struct contains configuration values to configure an OpenID Connect -/// (OIDC) authentication class. Required fields are the identity provider -/// (IdP) `hostname` and the TLS configuration. The `port` is selected -/// automatically if not configured otherwise. The `rootPath` defaults -/// to `/`. -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// Host of the identity provider, e.g. `my.keycloak.corp` or `127.0.0.1`. - hostname: HostName, - - /// Port of the identity provider. If TLS is used defaults to 443, - /// otherwise to 80. - port: Option, - - /// Root HTTP path of the identity provider. Defaults to `/`. - #[serde(default = "default_root_path")] - root_path: String, - - /// Use a TLS connection. If not specified no TLS will be used. - #[serde(flatten)] - pub tls: TlsClientDetails, - - /// If a product extracts some sort of "effective user" that is represented by a - /// string internally, this config determines with claim is used to extract that - /// string. It is desirable to use `sub` in here (or some other stable identifier), - /// but in many cases you might need to use `preferred_username` (e.g. in case of Keycloak) - /// or a different claim instead. - /// - /// Please note that some products hard-coded the claim in their implementation, - /// so some product operators might error out if the product hardcodes a different - /// claim than configured here. - /// - /// We don't provide any default value, as there is no correct way of doing it - /// that works in all setups. Most demos will probably use `preferred_username`, - /// although `sub` being more desirable, but technically impossible with the current - /// behavior of the products. - pub principal_claim: String, - - /// Scopes to request from your identity provider. It is recommended to - /// request the `openid`, `email`, and `profile` scopes. - pub scopes: Vec, - - /// This is a hint about which identity provider is used by the - /// AuthenticationClass. Operators *can* opt to use this - /// value to enable known quirks around OIDC / OAuth authentication. - /// Not providing a hint means there is no hint and OIDC should be used as it is - /// intended to be used (via the `.well-known` discovery). - #[serde(default)] - pub provider_hint: Option, -} - -fn default_root_path() -> String { - "/".to_string() -} - impl AuthenticationProvider { pub fn new( hostname: HostName, @@ -241,40 +178,10 @@ impl AuthenticationProvider { }, ] } -} -/// An enum of supported OIDC or identity providers which can serve as a hint -/// in the product operator. Some products require special handling of -/// authentication related config options. This hint can be used to enable such -/// special handling. -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "PascalCase")] -pub enum IdentityProviderHint { - Keycloak, -} - -/// OIDC specific config options. These are set on the product config level. -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct ClientAuthenticationOptions { - /// A reference to the OIDC client credentials secret. The secret contains - /// the client id and secret. - #[serde(rename = "clientCredentialsSecret")] - pub client_credentials_secret_ref: String, - - /// An optional list of extra scopes which get merged with the scopes - /// defined in the [`AuthenticationClass`]. - #[serde(default)] - pub extra_scopes: Vec, - - // If desired, operators can add custom fields that are only needed for this specific product. - // They need to create a struct holding them and pass that as `T`. - #[serde(flatten)] - pub product_specific_fields: T, + pub(super) fn default_root_path() -> String { + "/".to_string() + } } #[cfg(test)] From b52b0105ec6d5ca41c2e7b762c189fc0797dbecf Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:17:10 +0100 Subject: [PATCH 11/22] chore: Version static auth provider --- .../{static_.rs => static/mod.rs} | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) rename crates/stackable-operator/src/crd/authentication/{static_.rs => static/mod.rs} (69%) diff --git a/crates/stackable-operator/src/crd/authentication/static_.rs b/crates/stackable-operator/src/crd/authentication/static/mod.rs similarity index 69% rename from crates/stackable-operator/src/crd/authentication/static_.rs rename to crates/stackable-operator/src/crd/authentication/static/mod.rs index 65b485420..aa53b9dbb 100644 --- a/crates/stackable-operator/src/crd/authentication/static_.rs +++ b/crates/stackable-operator/src/crd/authentication/static/mod.rs @@ -19,25 +19,30 @@ //! periodically converts the secret contents to the required product format. //! //! See + use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationProvider { - /// Secret providing the usernames and passwords. - /// The Secret must contain an entry for every user, with the key being the username and the value the password in plain text. - /// It must be located in the same namespace as the product using it. - pub user_credentials_secret: UserCredentialsSecretRef, -} +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationProvider { + /// Secret providing the usernames and passwords. + /// The Secret must contain an entry for every user, with the key being the username and the value the password in plain text. + /// It must be located in the same namespace as the product using it. + pub user_credentials_secret: UserCredentialsSecretRef, + } -#[derive( - Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -pub struct UserCredentialsSecretRef { - /// Name of the Secret. - pub name: String, + #[derive( + Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct UserCredentialsSecretRef { + /// Name of the Secret. + pub name: String, + } } From 59fac98a3b5f8f0c254bb3462211554a751b53e6 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:17:31 +0100 Subject: [PATCH 12/22] chore: Version TLS auth provider --- .../src/crd/authentication/{tls.rs => tls/mod.rs} | 2 ++ 1 file changed, 2 insertions(+) rename crates/stackable-operator/src/crd/authentication/{tls.rs => tls/mod.rs} (89%) diff --git a/crates/stackable-operator/src/crd/authentication/tls.rs b/crates/stackable-operator/src/crd/authentication/tls/mod.rs similarity index 89% rename from crates/stackable-operator/src/crd/authentication/tls.rs rename to crates/stackable-operator/src/crd/authentication/tls/mod.rs index 202e49a10..38bdcb633 100644 --- a/crates/stackable-operator/src/crd/authentication/tls.rs +++ b/crates/stackable-operator/src/crd/authentication/tls/mod.rs @@ -1,6 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; +#[versioned(version(name = "v1alpha1"))] #[derive( Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, )] From 86af9afeb826269d933c78754831074624168c3e Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:17:51 +0100 Subject: [PATCH 13/22] chore: Version core auth CRDs --- .../src/crd/authentication/core/mod.rs | 146 +++++++++++++ .../crd/authentication/core/v1alpha1_impl.rs | 80 ++++++++ .../src/crd/authentication/mod.rs | 193 +----------------- 3 files changed, 228 insertions(+), 191 deletions(-) create mode 100644 crates/stackable-operator/src/crd/authentication/core/mod.rs create mode 100644 crates/stackable-operator/src/crd/authentication/core/v1alpha1_impl.rs diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs new file mode 100644 index 000000000..244a5383f --- /dev/null +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -0,0 +1,146 @@ +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; + +mod v1alpha1_impl; + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + // This makes v1alpha1 versions of all authentication providers available to the + // AuthenticationClassProvider enum below. + mod v1alpha1 { + use crate::crd::authentication::{ + kerberos::v1alpha1 as kerberos_v1alpha1, ldap::v1alpha1 as ldap_v1alpha1, + oidc::v1alpha1 as oidc_v1alpha1, r#static::v1alpha1 as static_v1alpha1, + tls::v1alpha1 as tls_v1alpha1, + }; + } + /// The Stackable Platform uses the AuthenticationClass as a central mechanism to handle user + /// authentication across supported products. + /// + /// The authentication mechanism needs to be configured only in the AuthenticationClass which is + /// then referenced in the product. Multiple different authentication providers are supported. + /// Learn more in the [authentication concept documentation][1] and the + /// [Authentication with OpenLDAP tutorial][2]. + /// + /// [1]: DOCS_BASE_URL_PLACEHOLDER/concepts/authentication + /// [2]: DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap + #[versioned(k8s( + group = "authentication.stackable.tech", + plural = "authenticationclasses", + crates( + kube_core = "kube::core", + k8s_openapi = "k8s_openapi", + schemars = "schemars" + ) + ))] + #[derive( + Clone, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + CustomResource, + Deserialize, + JsonSchema, + Serialize, + )] + #[serde(rename_all = "camelCase")] + pub struct AuthenticationClassSpec { + /// Provider used for authentication like LDAP or Kerberos. + pub provider: AuthenticationClassProvider, + } + + #[derive( + Clone, + Debug, + Deserialize, + strum::Display, + Eq, + Hash, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, + )] + #[serde(rename_all = "camelCase")] + #[allow(clippy::large_enum_variant)] + pub enum AuthenticationClassProvider { + /// The [static provider](https://DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_static) + /// is used to configure a static set of users, identified by username and password. + Static(static_v1alpha1::AuthenticationProvider), + + /// The [LDAP provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_ldap). + /// There is also the ["Authentication with LDAP" tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap) + /// where you can learn to configure Superset and Trino with OpenLDAP. + Ldap(ldap_v1alpha1::AuthenticationProvider), + + /// The OIDC provider can be used to configure OpenID Connect. + Oidc(oidc_v1alpha1::AuthenticationProvider), + + /// The [TLS provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_tls). + /// The TLS AuthenticationClass is used when users should authenticate themselves with a TLS certificate. + Tls(tls_v1alpha1::AuthenticationProvider), + + /// The [Kerberos provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_kerberos). + /// The Kerberos AuthenticationClass is used when users should authenticate themselves via Kerberos. + Kerberos(kerberos_v1alpha1::AuthenticationProvider), + } + + /// Common [`ClientAuthenticationDetails`] which is specified at the client/ + /// product cluster level. It provides a name (key) to resolve a particular + /// [`AuthenticationClass`]. Additionally, it provides authentication provider + /// specific configuration (OIDC and LDAP for example). + /// + /// If the product needs additional (product specific) authentication options, + /// it is recommended to wrap this struct and use `#[serde(flatten)]` on the + /// field. + /// + /// Additionally, it might be the case that special fields are needed in the + /// contained structs, such as [`oidc::ClientAuthenticationOptions`]. To be able + /// to add custom fields in that structs without serde(flattening) multiple structs, + /// they are generic, so you can add additional attributes if needed. + /// + /// ### Example + /// + /// ``` + /// # use schemars::JsonSchema; + /// # use serde::{Deserialize, Serialize}; + /// use stackable_operator::crd::authentication::ClientAuthenticationDetails; + /// + /// #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + /// #[serde(rename_all = "camelCase")] + /// pub struct SupersetAuthenticationClass { + /// pub user_registration_role: String, + /// pub user_registration: bool, + /// + /// #[serde(flatten)] + /// pub common: ClientAuthenticationDetails, + /// } + /// ``` + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + #[schemars(description = "")] + pub struct ClientAuthenticationDetails { + /// Name of the [AuthenticationClass](https://docs.stackable.tech/home/nightly/concepts/authentication) used to + /// authenticate users. + // + // To get the concrete [`AuthenticationClass`], we must resolve it. This resolution can be achieved by using + // [`ClientAuthenticationDetails::resolve_class`]. + #[serde(rename = "authenticationClass")] + authentication_class_ref: String, + + /// This field contains OIDC-specific configuration. It is only required in case OIDC is used. + // + // Use [`ClientAuthenticationDetails::oidc_or_error`] to get the value or report an error to the user. + // TODO: Ideally we want this to be an enum once other `ClientAuthenticationOptions` are added, so + // that user can not configure multiple options at the same time (yes we are aware that this makes a + // changing the type of an AuthenticationClass harder). + // This is a non-breaking change though :) + oidc: Option>, + } +} diff --git a/crates/stackable-operator/src/crd/authentication/core/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/authentication/core/v1alpha1_impl.rs new file mode 100644 index 000000000..f84ef113c --- /dev/null +++ b/crates/stackable-operator/src/crd/authentication/core/v1alpha1_impl.rs @@ -0,0 +1,80 @@ +use snafu::{OptionExt as _, Snafu}; + +use crate::{ + client::Client, + crd::authentication::{ + core::v1alpha1::{AuthenticationClass, ClientAuthenticationDetails}, + oidc::v1alpha1 as oidc_v1alpha1, + }, +}; + +type Result = std::result::Result; + +// NOTE (@Techassi): Where is the best place to put this? +#[derive(Debug, PartialEq, Snafu)] +pub enum Error { + #[snafu(display("authentication details for OIDC were not specified. The AuthenticationClass {auth_class_name:?} uses an OIDC provider, you need to specify OIDC authentication details (such as client credentials) as well"))] + OidcAuthenticationDetailsNotSpecified { auth_class_name: String }, +} + +impl AuthenticationClass { + pub async fn resolve( + client: &Client, + authentication_class_name: &str, + ) -> crate::client::Result { + client + .get::(authentication_class_name, &()) // AuthenticationClass has ClusterScope + .await + } +} + +impl ClientAuthenticationDetails { + /// Resolves this specific [`AuthenticationClass`]. Usually products support + /// a list of authentication classes, which individually need to be resolved.crate::client + pub async fn resolve_class( + &self, + client: &Client, + ) -> crate::client::Result { + AuthenticationClass::resolve(client, &self.authentication_class_ref).await + } + + pub fn authentication_class_name(&self) -> &String { + &self.authentication_class_ref + } + + /// In case OIDC is configured, the user *needs* to provide some connection details, + /// such as the client credentials. Call this function in case the user has configured + /// OIDC, as it will error out then the OIDC client details are missing. + pub fn oidc_or_error( + &self, + auth_class_name: &str, + ) -> Result<&oidc_v1alpha1::ClientAuthenticationOptions> { + self.oidc + .as_ref() + .with_context(|| OidcAuthenticationDetailsNotSpecifiedSnafu { + auth_class_name: auth_class_name.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::crd::authentication::{ + core::v1alpha1::AuthenticationClassProvider, kerberos::v1alpha1 as kerberos_v1alpha1, + tls::v1alpha1 as tls_v1alpha1, + }; + + #[test] + fn provider_to_string() { + let tls_provider = AuthenticationClassProvider::Tls(tls_v1alpha1::AuthenticationProvider { + client_cert_secret_class: None, + }); + assert_eq!("Tls", tls_provider.to_string()); + + let kerberos_provider = + AuthenticationClassProvider::Kerberos(kerberos_v1alpha1::AuthenticationProvider { + kerberos_secret_class: "kerberos".to_string(), + }); + assert_eq!("Kerberos", kerberos_provider.to_string()); + } +} diff --git a/crates/stackable-operator/src/crd/authentication/mod.rs b/crates/stackable-operator/src/crd/authentication/mod.rs index 829973572..1e2a61316 100644 --- a/crates/stackable-operator/src/crd/authentication/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/mod.rs @@ -1,195 +1,6 @@ -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, Snafu}; -use strum::Display; - -use crate::client::Client; - +pub mod core; pub mod kerberos; pub mod ldap; pub mod oidc; -pub mod static_; +pub mod r#static; pub mod tls; - -type Result = std::result::Result; - -#[derive(Debug, PartialEq, Snafu)] -pub enum Error { - #[snafu(display("authentication details for OIDC were not specified. The AuthenticationClass {auth_class_name:?} uses an OIDC provider, you need to specify OIDC authentication details (such as client credentials) as well"))] - OidcAuthenticationDetailsNotSpecified { auth_class_name: String }, -} - -/// The Stackable Platform uses the AuthenticationClass as a central mechanism to handle user authentication across supported products. -/// The authentication mechanism needs to be configured only in the AuthenticationClass which is then referenced in the product. -/// Multiple different authentication providers are supported. -/// Learn more in the [authentication concept documentation](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication) and the -/// [Authentication with OpenLDAP tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap). -#[derive( - Clone, - CustomResource, - Debug, - Deserialize, - Eq, - Hash, - JsonSchema, - Ord, - PartialEq, - PartialOrd, - Serialize, -)] -#[kube( - group = "authentication.stackable.tech", - version = "v1alpha1", - kind = "AuthenticationClass", - plural = "authenticationclasses", - crates( - kube_core = "kube::core", - k8s_openapi = "k8s_openapi", - schemars = "schemars" - ) -)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticationClassSpec { - /// Provider used for authentication like LDAP or Kerberos. - pub provider: AuthenticationClassProvider, -} - -#[derive( - Clone, Debug, Deserialize, Display, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize, -)] -#[serde(rename_all = "camelCase")] -#[allow(clippy::large_enum_variant)] -pub enum AuthenticationClassProvider { - /// The [static provider](https://DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_static) is used to configure a - /// static set of users, identified by username and password. - Static(static_::AuthenticationProvider), - - /// The [LDAP provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_ldap). - /// There is also the ["Authentication with LDAP" tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap) - /// where you can learn to configure Superset and Trino with OpenLDAP. - Ldap(ldap::AuthenticationProvider), - - /// The OIDC provider can be used to configure OpenID Connect. - Oidc(oidc::AuthenticationProvider), - - /// The [TLS provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_tls). - /// The TLS AuthenticationClass is used when users should authenticate themselves with a TLS certificate. - Tls(tls::AuthenticationProvider), - - /// The [Kerberos provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_kerberos). - /// The Kerberos AuthenticationClass is used when users should authenticate themselves via Kerberos. - Kerberos(kerberos::AuthenticationProvider), -} - -impl AuthenticationClass { - pub async fn resolve( - client: &Client, - authentication_class_name: &str, - ) -> crate::client::Result { - client - .get::(authentication_class_name, &()) // AuthenticationClass has ClusterScope - .await - } -} - -/// Common [`ClientAuthenticationDetails`] which is specified at the client/ -/// product cluster level. It provides a name (key) to resolve a particular -/// [`AuthenticationClass`]. Additionally, it provides authentication provider -/// specific configuration (OIDC and LDAP for example). -/// -/// If the product needs additional (product specific) authentication options, -/// it is recommended to wrap this struct and use `#[serde(flatten)]` on the -/// field. -/// -/// Additionally, it might be the case that special fields are needed in the -/// contained structs, such as [`oidc::ClientAuthenticationOptions`]. To be able -/// to add custom fields in that structs without serde(flattening) multiple structs, -/// they are generic, so you can add additional attributes if needed. -/// -/// ### Example -/// -/// ``` -/// # use schemars::JsonSchema; -/// # use serde::{Deserialize, Serialize}; -/// use stackable_operator::crd::authentication::ClientAuthenticationDetails; -/// -/// #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -/// #[serde(rename_all = "camelCase")] -/// pub struct SupersetAuthenticationClass { -/// pub user_registration_role: String, -/// pub user_registration: bool, -/// -/// #[serde(flatten)] -/// pub common: ClientAuthenticationDetails, -/// } -/// ``` -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -#[schemars(description = "")] -pub struct ClientAuthenticationDetails { - /// Name of the [AuthenticationClass](https://docs.stackable.tech/home/nightly/concepts/authentication) used to - /// authenticate users. - // - // To get the concrete [`AuthenticationClass`], we must resolve it. This resolution can be achieved by using - // [`ClientAuthenticationDetails::resolve_class`]. - #[serde(rename = "authenticationClass")] - authentication_class_ref: String, - - /// This field contains OIDC-specific configuration. It is only required in case OIDC is used. - // - // Use [`ClientAuthenticationDetails::oidc_or_error`] to get the value or report an error to the user. - // TODO: Ideally we want this to be an enum once other `ClientAuthenticationOptions` are added, so - // that user can not configure multiple options at the same time (yes we are aware that this makes a - // changing the type of an AuthenticationClass harder). - // This is a non-breaking change though :) - oidc: Option>, -} - -impl ClientAuthenticationDetails { - /// Resolves this specific [`AuthenticationClass`]. Usually products support - /// a list of authentication classes, which individually need to be resolved.crate::client - pub async fn resolve_class( - &self, - client: &Client, - ) -> crate::client::Result { - AuthenticationClass::resolve(client, &self.authentication_class_ref).await - } - - pub fn authentication_class_name(&self) -> &String { - &self.authentication_class_ref - } - - /// In case OIDC is configured, the user *needs* to provide some connection details, - /// such as the client credentials. Call this function in case the user has configured - /// OIDC, as it will error out then the OIDC client details are missing. - pub fn oidc_or_error( - &self, - auth_class_name: &str, - ) -> Result<&oidc::ClientAuthenticationOptions> { - self.oidc - .as_ref() - .with_context(|| OidcAuthenticationDetailsNotSpecifiedSnafu { - auth_class_name: auth_class_name.to_string(), - }) - } -} - -#[cfg(test)] -mod tests { - use crate::crd::authentication::{kerberos, tls, AuthenticationClassProvider}; - - #[test] - fn provider_to_string() { - let tls_provider = AuthenticationClassProvider::Tls(tls::AuthenticationProvider { - client_cert_secret_class: None, - }); - assert_eq!("Tls", tls_provider.to_string()); - - let kerberos_provider = - AuthenticationClassProvider::Kerberos(kerberos::AuthenticationProvider { - kerberos_secret_class: "kerberos".to_string(), - }); - assert_eq!("Kerberos", kerberos_provider.to_string()); - } -} From ee8ccd9c95aabf93f0715c24cdd285d0f0e73f4b Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:35:09 +0100 Subject: [PATCH 14/22] docs: Fix doc comments --- .../src/crd/authentication/core/mod.rs | 21 +++++++++---------- .../src/crd/authentication/oidc/mod.rs | 6 +++--- crates/stackable-versioned-macros/src/lib.rs | 7 +++++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index 244a5383f..eaf64f721 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -91,19 +91,18 @@ pub mod versioned { Kerberos(kerberos_v1alpha1::AuthenticationProvider), } - /// Common [`ClientAuthenticationDetails`] which is specified at the client/ - /// product cluster level. It provides a name (key) to resolve a particular - /// [`AuthenticationClass`]. Additionally, it provides authentication provider - /// specific configuration (OIDC and LDAP for example). + /// Common [`v1alpha1::ClientAuthenticationDetails`] which is specified at the client/ product + /// cluster level. It provides a name (key) to resolve a particular [`AuthenticationClass`]. + /// Additionally, it provides authentication provider specific configuration (OIDC and LDAP for + /// example). /// - /// If the product needs additional (product specific) authentication options, - /// it is recommended to wrap this struct and use `#[serde(flatten)]` on the - /// field. + /// If the product needs additional (product specific) authentication options, it is recommended + /// to wrap this struct and use `#[serde(flatten)]` on the field. /// - /// Additionally, it might be the case that special fields are needed in the - /// contained structs, such as [`oidc::ClientAuthenticationOptions`]. To be able - /// to add custom fields in that structs without serde(flattening) multiple structs, - /// they are generic, so you can add additional attributes if needed. + /// Additionally, it might be the case that special fields are needed in the contained structs, + /// such as [`oidc_v1alpha1::ClientAuthenticationOptions`]. To be able to add custom fields in + /// that structs without serde(flattening) multiple structs, they are generic, so you can add + /// additional attributes if needed. /// /// ### Example /// diff --git a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs index e56a15c5a..73986de79 100644 --- a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs @@ -3,8 +3,6 @@ use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; use crate::commons::{networking::HostName, tls_verification::TlsClientDetails}; -#[cfg(doc)] -use crate::crd::authentication::AuthenticationClass; mod v1alpha1_impl; @@ -95,7 +93,9 @@ pub mod versioned { pub client_credentials_secret_ref: String, /// An optional list of extra scopes which get merged with the scopes - /// defined in the [`AuthenticationClass`]. + /// defined in the [`AuthenticationClass`][1]. + /// + /// [1]: crate::crd::authentication::core::v1alpha1::AuthenticationClass #[serde(default)] pub extra_scopes: Vec, diff --git a/crates/stackable-versioned-macros/src/lib.rs b/crates/stackable-versioned-macros/src/lib.rs index 0d67b8ef7..b01cde20d 100644 --- a/crates/stackable-versioned-macros/src/lib.rs +++ b/crates/stackable-versioned-macros/src/lib.rs @@ -700,7 +700,7 @@ especially for CustomResourceDefinitions (CRDs). These features are completely opt-in. You need to enable the `k8s` feature (which enables optional dependencies) and use the `k8s()` parameter in the macro. -You need to derive both [`kube::CustomResource`] and [`schemars::JsonSchema`]. +You need to derive both [`kube::CustomResource`] and [`schemars::JsonSchema`][1]. ``` # use stackable_versioned_macros::versioned; @@ -730,7 +730,7 @@ println!("{}", serde_yaml::to_string(&merged_crd).unwrap()); # } ``` -The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][1] +The generated `merged_crd` method is a wrapper around [kube's `merge_crds`][2] function. It automatically calls the `crd` methods of the CRD in all of its versions and additionally provides a strongly typed selector for the stored API version. @@ -846,6 +846,9 @@ mod v1 { It is possible to include structs and enums which are not CRDs. They are instead versioned as expected (without adding the `#[kube]` derive macro and generating code to merge CRD versions). + +[1]: https://docs.rs/schemars/latest/schemars/derive.JsonSchema.html +[2]: https://docs.rs/kube/latest/kube/core/crd/fn.merge_crds.html "# )] #[proc_macro_attribute] From 4c036df0b1456fc091d41ae1d1c489c5f3f81b86 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Mar 2025 16:40:44 +0100 Subject: [PATCH 15/22] docs: Import url::Url for docs only --- crates/stackable-operator/src/crd/authentication/oidc/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs index 73986de79..3885ba0cf 100644 --- a/crates/stackable-operator/src/crd/authentication/oidc/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/oidc/mod.rs @@ -1,6 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use stackable_versioned::versioned; +#[cfg(doc)] +use url::Url; use crate::commons::{networking::HostName, tls_verification::TlsClientDetails}; From a4c3bdc6e657257d9f2a6915df042dc2d1d3d203 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 16:43:45 +0200 Subject: [PATCH 16/22] chore: Version Listener CRDs --- .../src/cluster_resources.rs | 13 +- .../src/crd/listener/class.rs | 61 ------- .../src/crd/listener/class/mod.rs | 55 ++++++ .../src/crd/listener/class/v1alpha1_impl.rs | 19 ++ .../src/crd/listener/core/mod.rs | 83 +++++++++ .../src/crd/listener/core/v1alpha1_impl.rs | 17 ++ .../src/crd/listener/listeners.rs | 132 -------------- .../src/crd/listener/listeners/mod.rs | 165 ++++++++++++++++++ .../crd/listener/listeners/v1alpha1_impl.rs | 7 + .../src/crd/listener/mod.rs | 106 +---------- 10 files changed, 354 insertions(+), 304 deletions(-) delete mode 100644 crates/stackable-operator/src/crd/listener/class.rs create mode 100644 crates/stackable-operator/src/crd/listener/class/mod.rs create mode 100644 crates/stackable-operator/src/crd/listener/class/v1alpha1_impl.rs create mode 100644 crates/stackable-operator/src/crd/listener/core/mod.rs create mode 100644 crates/stackable-operator/src/crd/listener/core/v1alpha1_impl.rs delete mode 100644 crates/stackable-operator/src/crd/listener/listeners.rs create mode 100644 crates/stackable-operator/src/crd/listener/listeners/mod.rs create mode 100644 crates/stackable-operator/src/crd/listener/listeners/v1alpha1_impl.rs diff --git a/crates/stackable-operator/src/cluster_resources.rs b/crates/stackable-operator/src/cluster_resources.rs index 207ee395a..8fea00986 100644 --- a/crates/stackable-operator/src/cluster_resources.rs +++ b/crates/stackable-operator/src/cluster_resources.rs @@ -5,6 +5,8 @@ use std::{ fmt::Debug, }; +#[cfg(doc)] +use k8s_openapi::api::core::v1::{NodeSelector, Pod}; use k8s_openapi::{ api::{ apps::v1::{ @@ -26,11 +28,6 @@ use snafu::{OptionExt, ResultExt, Snafu}; use strum::Display; use tracing::{debug, info, warn}; -#[cfg(doc)] -use crate::k8s_openapi::api::{ - apps::v1::Deployment, - core::v1::{NodeSelector, Pod}, -}; use crate::{ client::{Client, GetApi}, commons::{ @@ -40,7 +37,7 @@ use crate::{ LIMIT_REQUEST_RATIO_CPU, LIMIT_REQUEST_RATIO_MEMORY, }, }, - crd::listener::Listener, + crd::listener::v1alpha1 as listener_v1alpha1, kvp::{ consts::{K8S_APP_INSTANCE_KEY, K8S_APP_MANAGED_BY_KEY, K8S_APP_NAME_KEY}, Label, LabelError, Labels, @@ -206,7 +203,7 @@ impl ClusterResource for Service {} impl ClusterResource for ServiceAccount {} impl ClusterResource for RoleBinding {} impl ClusterResource for PodDisruptionBudget {} -impl ClusterResource for Listener {} +impl ClusterResource for listener_v1alpha1::Listener {} impl ClusterResource for Job { fn pod_spec(&self) -> Option<&PodSpec> { @@ -647,7 +644,7 @@ impl ClusterResources { self.delete_orphaned_resources_of_kind::(client), self.delete_orphaned_resources_of_kind::(client), self.delete_orphaned_resources_of_kind::(client), - self.delete_orphaned_resources_of_kind::(client), + self.delete_orphaned_resources_of_kind::(client), )?; Ok(()) diff --git a/crates/stackable-operator/src/crd/listener/class.rs b/crates/stackable-operator/src/crd/listener/class.rs deleted file mode 100644 index e5414dac1..000000000 --- a/crates/stackable-operator/src/crd/listener/class.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::collections::BTreeMap; - -#[cfg(doc)] -use k8s_openapi::api::core::v1::Service; -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::crd::listener::{ - AddressType, KubernetesTrafficPolicy, PreferredAddressType, ServiceType, -}; - -/// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed. -/// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass) -/// for more information. -#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[kube( - group = "listeners.stackable.tech", - version = "v1alpha1", - kind = "ListenerClass" -)] -#[serde(rename_all = "camelCase")] -pub struct ListenerClassSpec { - pub service_type: ServiceType, - - /// Annotations that should be added to the Service object. - #[serde(default)] - pub service_annotations: BTreeMap, - - /// `externalTrafficPolicy` that should be set on the created [`Service`] objects. - /// - /// The default is `Local` (in contrast to `Cluster`), as we aim to direct traffic to a node running the workload - /// and we should keep testing that as the primary configuration. Cluster is a fallback option for providers that - /// break Local mode (IONOS so far). - #[serde(default = "ListenerClassSpec::default_service_external_traffic_policy")] - pub service_external_traffic_policy: KubernetesTrafficPolicy, - - /// Whether addresses should prefer using the IP address (`IP`) or the hostname (`Hostname`). - /// Can also be set to `HostnameConservative`, which will use `IP` for `NodePort` service types, but `Hostname` for everything else. - /// - /// The other type will be used if the preferred type is not available. - /// - /// Defaults to `HostnameConservative`. - #[serde(default = "ListenerClassSpec::default_preferred_address_type")] - pub preferred_address_type: PreferredAddressType, -} - -impl ListenerClassSpec { - const fn default_service_external_traffic_policy() -> KubernetesTrafficPolicy { - KubernetesTrafficPolicy::Local - } - - const fn default_preferred_address_type() -> PreferredAddressType { - PreferredAddressType::HostnameConservative - } - - /// Resolves [`Self::preferred_address_type`]'s "smart" modes depending on the rest of `self`. - pub fn resolve_preferred_address_type(&self) -> AddressType { - self.preferred_address_type.resolve(self) - } -} diff --git a/crates/stackable-operator/src/crd/listener/class/mod.rs b/crates/stackable-operator/src/crd/listener/class/mod.rs new file mode 100644 index 000000000..a520ff087 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/class/mod.rs @@ -0,0 +1,55 @@ +//! This module contains resource types to interact with [`v1alpha1::ListenerClass`]es. +//! +//! It declares a policy for how [`v1alpha1::Listener`][listener]s are exposed to users. It is +//! created by the cluster administrator. +//! +//! [listener]: crate::crd::listener::listeners::v1alpha1::Listener + +use std::collections::BTreeMap; + +#[cfg(doc)] +use k8s_openapi::api::core::v1::Service; +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; + +use crate::crd::listener::core::v1alpha1 as core_v1alpha1; +#[cfg(doc)] +use crate::crd::listener::listeners::v1alpha1::Listener; + +mod v1alpha1_impl; + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// Defines a policy for how [Listeners](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener) should be exposed. + /// Read the [ListenerClass documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass) + /// for more information. + #[versioned(k8s(group = "listeners.stackable.tech"))] + #[derive(CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + pub struct ListenerClassSpec { + pub service_type: core_v1alpha1::ServiceType, + + /// Annotations that should be added to the Service object. + #[serde(default)] + pub service_annotations: BTreeMap, + + /// `externalTrafficPolicy` that should be set on the created [`Service`] objects. + /// + /// The default is `Local` (in contrast to `Cluster`), as we aim to direct traffic to a node running the workload + /// and we should keep testing that as the primary configuration. Cluster is a fallback option for providers that + /// break Local mode (IONOS so far). + #[serde(default = "ListenerClassSpec::default_service_external_traffic_policy")] + pub service_external_traffic_policy: core_v1alpha1::KubernetesTrafficPolicy, + + /// Whether addresses should prefer using the IP address (`IP`) or the hostname (`Hostname`). + /// Can also be set to `HostnameConservative`, which will use `IP` for `NodePort` service types, but `Hostname` for everything else. + /// + /// The other type will be used if the preferred type is not available. + /// + /// Defaults to `HostnameConservative`. + #[serde(default = "ListenerClassSpec::default_preferred_address_type")] + pub preferred_address_type: core_v1alpha1::PreferredAddressType, + } +} diff --git a/crates/stackable-operator/src/crd/listener/class/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/listener/class/v1alpha1_impl.rs new file mode 100644 index 000000000..56cb57780 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/class/v1alpha1_impl.rs @@ -0,0 +1,19 @@ +use crate::crd::listener::{ + class::v1alpha1::ListenerClassSpec, + core::v1alpha1::{AddressType, KubernetesTrafficPolicy, PreferredAddressType}, +}; + +impl ListenerClassSpec { + pub(super) const fn default_service_external_traffic_policy() -> KubernetesTrafficPolicy { + KubernetesTrafficPolicy::Local + } + + pub(super) const fn default_preferred_address_type() -> PreferredAddressType { + PreferredAddressType::HostnameConservative + } + + /// Resolves [`Self::preferred_address_type`]'s "smart" modes depending on the rest of `self`. + pub fn resolve_preferred_address_type(&self) -> AddressType { + self.preferred_address_type.resolve(self) + } +} diff --git a/crates/stackable-operator/src/crd/listener/core/mod.rs b/crates/stackable-operator/src/crd/listener/core/mod.rs new file mode 100644 index 000000000..394aae48d --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/core/mod.rs @@ -0,0 +1,83 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; + +#[cfg(doc)] +use crate::crd::listener::class::v1alpha1::ListenerClass; + +mod v1alpha1_impl; + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// The method used to access the services. + // + // Please note that this does not necessarily need to be restricted to the same Service types + // Kubernetes supports. Listeners currently happens to support the same set of service types as + // upstream Kubernetes, but we still want to have the freedom to add custom ones in the future + // (for example: Istio ingress?). + #[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] + pub enum ServiceType { + /// Reserve a port on each node. + NodePort, + + /// Provision a dedicated load balancer. + LoadBalancer, + + /// Assigns an IP address from a pool of IP addresses that your cluster has reserved for + /// that purpose. + ClusterIP, + } + + /// Service Internal Traffic Policy enables internal traffic restrictions to only route internal + /// traffic to endpoints within the node the traffic originated from. The "internal" traffic + /// here refers to traffic originated from Pods in the current cluster. This can help to reduce + /// costs and improve performance. See [Kubernetes docs][k8s-docs]. + /// + /// [k8s-docs]: https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/ + // + // Please note that this represents a Kubernetes type, so the name of the enum variant needs to + // exactly match the Kubernetes traffic policy. + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq, strum::Display)] + pub enum KubernetesTrafficPolicy { + /// Obscures the client source IP and may cause a second hop to another node, but allows + /// Kubernetes to spread the load between all nodes. + Cluster, + + /// Preserves the client source IP and avoid a second hop for LoadBalancer and NodePort type + /// Services, but makes clients responsible for spreading the load. + Local, + } + + /// The type of a given address. + #[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "PascalCase")] + pub enum AddressType { + /// A resolvable DNS hostname. + Hostname, + + /// A resolved IP address. + #[serde(rename = "IP")] + Ip, + } + + /// A mode for deciding the preferred [`v1alpha1::AddressType`]. + /// + /// These can vary depending on the rest of the [`ListenerClass`][lc]. + /// + /// [lc]: crate::crd::listener::class::v1alpha1::ListenerClass + #[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] + pub enum PreferredAddressType { + /// Like [`AddressType::Hostname`], but prefers [`AddressType::Ip`] for [`ServiceType::NodePort`], since their hostnames are less likely to be resolvable. + HostnameConservative, + + // Like the respective variants of AddressType. Ideally we would refer to them instead of + // copy/pasting, but that breaks due to upstream issues: + // + // - https://github.com/GREsau/schemars/issues/222 + // - https://github.com/kube-rs/kube/issues/1622 + Hostname, + + #[serde(rename = "IP")] + Ip, + } +} diff --git a/crates/stackable-operator/src/crd/listener/core/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/listener/core/v1alpha1_impl.rs new file mode 100644 index 000000000..a6bd85198 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/core/v1alpha1_impl.rs @@ -0,0 +1,17 @@ +use crate::crd::listener::{ + class::v1alpha1::ListenerClassSpec, + core::v1alpha1::{AddressType, PreferredAddressType, ServiceType}, +}; + +impl PreferredAddressType { + pub fn resolve(self, listener_class: &ListenerClassSpec) -> AddressType { + match self { + PreferredAddressType::HostnameConservative => match listener_class.service_type { + ServiceType::NodePort => AddressType::Ip, + _ => AddressType::Hostname, + }, + PreferredAddressType::Hostname => AddressType::Hostname, + PreferredAddressType::Ip => AddressType::Ip, + } + } +} diff --git a/crates/stackable-operator/src/crd/listener/listeners.rs b/crates/stackable-operator/src/crd/listener/listeners.rs deleted file mode 100644 index cf696c41f..000000000 --- a/crates/stackable-operator/src/crd/listener/listeners.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::collections::BTreeMap; - -use kube::CustomResource; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::crd::listener::AddressType; - -/// Exposes a set of pods to the outside world. -/// -/// Essentially a Stackable extension of a Kubernetes Service. Compared to a Service, a Listener changes three things: -/// 1. It uses a cluster-level policy object (ListenerClass) to define how exactly the exposure works -/// 2. It has a consistent API for reading back the exposed address(es) of the service -/// 3. The Pod must mount a Volume referring to the Listener, which also allows -/// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling). -/// -/// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener). -#[derive( - CustomResource, Serialize, Deserialize, Default, Clone, Debug, JsonSchema, PartialEq, Eq, -)] -#[kube( - group = "listeners.stackable.tech", - version = "v1alpha1", - kind = "Listener", - namespaced, - status = "ListenerStatus" -)] -#[serde(rename_all = "camelCase")] -pub struct ListenerSpec { - /// The name of the [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass). - pub class_name: Option, - - /// Extra labels that the Pods must match in order to be exposed. They must _also_ still have a Volume referring to the Listener. - #[serde(default)] - pub extra_pod_selector_labels: BTreeMap, - - /// Ports that should be exposed. - pub ports: Option>, - - /// Whether incoming traffic should also be directed to Pods that are not `Ready`. - #[serde(default = "ListenerSpec::default_publish_not_ready_addresses")] - pub publish_not_ready_addresses: Option, -} - -impl ListenerSpec { - const fn default_publish_not_ready_addresses() -> Option { - Some(true) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ListenerPort { - /// The name of the port. - /// - /// The name of each port *must* be unique within a single Listener. - pub name: String, - /// The port number. - pub port: i32, - /// The layer-4 protocol (`TCP` or `UDP`). - pub protocol: Option, -} - -/// Informs users about how to reach the Listener. -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ListenerStatus { - /// The backing Kubernetes Service. - pub service_name: Option, - /// All addresses that the Listener is currently reachable from. - pub ingress_addresses: Option>, - /// Port mappings for accessing the Listener on each Node that the Pods are currently running on. - /// - /// This is only intended for internal use by listener-operator itself. This will be left unset if using a ListenerClass that does - /// not require Node-local access. - pub node_ports: Option>, -} - -/// One address that a Listener is accessible from. -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ListenerIngress { - /// The hostname or IP address to the Listener. - pub address: String, - /// The type of address (`Hostname` or `IP`). - pub address_type: AddressType, - /// Port mapping table. - pub ports: BTreeMap, -} - -/// Informs users about Listeners that are bound by a given Pod. -/// -/// This is not expected to be created or modified by users. It will be created by -/// the Stackable Listener Operator when mounting the listener volume, and is always -/// named `pod-{pod.metadata.uid}`. -#[derive( - CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, Default, PartialEq, Eq, -)] -#[kube( - group = "listeners.stackable.tech", - version = "v1alpha1", - kind = "PodListeners", - namespaced, - plural = "podlisteners" -)] -#[serde(rename_all = "camelCase")] -pub struct PodListenersSpec { - /// All Listeners currently bound by the Pod. - /// - /// Indexed by Volume name (not PersistentVolume or PersistentVolumeClaim). - pub listeners: BTreeMap, -} - -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct PodListener { - /// `Node` if this address only allows access to Pods hosted on a specific Kubernetes Node, otherwise `Cluster`. - pub scope: PodListenerScope, - /// Addresses allowing access to this Pod. - /// - /// Compared to `ingress_addresses` on the Listener status, this list is restricted to addresses that can access this Pod. - /// - /// This field is intended to be equivalent to the files mounted into the Listener volume. - pub ingress_addresses: Option>, -} - -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "PascalCase")] -pub enum PodListenerScope { - Node, - Cluster, -} diff --git a/crates/stackable-operator/src/crd/listener/listeners/mod.rs b/crates/stackable-operator/src/crd/listener/listeners/mod.rs new file mode 100644 index 000000000..095eeb9d0 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/listeners/mod.rs @@ -0,0 +1,165 @@ +//! This module provides resource types to interact with [`v1alpha1::Listener`]s and +//! [`v1alpha1::PodListener`]s. +//! +//! ## [`v1alpha1::Listener`] +//! +//! Exposes a set of pods, either internally to the cluster or to the outside world. The mechanism +//! for how it is exposed is managed by the [`v1alpha1::ListenerClass`][lc]. +//! +//! It can be either created manually by the application administrator (for applications that expose +//! a single load-balanced endpoint), or automatically when mounting a [listener volume][lvb] (for +//! applications that expose a separate endpoint per replica). +//! +//! All exposed pods *must* have a mounted [listener volume][lvb], regardless of whether the +//! [`v1alpha1::Listener`] is created automatically. +//! +//! ## [`v1alpha1::PodListeners`] +//! +//! Informs users and other operators about the state of all [`v1alpha1::Listener`]s associated with +//! a [`Pod`]. +//! +//! It is created by the Stackable Secret Operator, and always named `pod-{pod.metadata.uid}`. +//! +//! [lc]: crate::crd::listener::class::v1alpha1::ListenerClass +//! [lvb]: ListenerOperatorVolumeSourceBuilder + +use std::collections::BTreeMap; + +#[cfg(doc)] +use k8s_openapi::api::core::v1::Pod; +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use stackable_versioned::versioned; + +#[cfg(doc)] +use crate::builder::pod::volume::ListenerOperatorVolumeSourceBuilder; +use crate::crd::listener::core::v1alpha1 as core_v1alpha1; + +mod v1alpha1_impl; + +#[versioned(version(name = "v1alpha1"))] +pub mod versioned { + /// Exposes a set of pods to the outside world. + /// + /// Essentially a Stackable extension of a Kubernetes Service. Compared to a Service, a Listener changes three things: + /// 1. It uses a cluster-level policy object (ListenerClass) to define how exactly the exposure works + /// 2. It has a consistent API for reading back the exposed address(es) of the service + /// 3. The Pod must mount a Volume referring to the Listener, which also allows + /// ["sticky" scheduling](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener#_sticky_scheduling). + /// + /// Learn more in the [Listener documentation](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listener). + #[versioned(k8s( + group = "listeners.stackable.tech", + status = "v1alpha1::ListenerStatus", + namespaced + ))] + #[derive( + CustomResource, Serialize, Deserialize, Default, Clone, Debug, JsonSchema, PartialEq, Eq, + )] + #[serde(rename_all = "camelCase")] + pub struct ListenerSpec { + /// The name of the [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass). + pub class_name: Option, + + /// Extra labels that the Pods must match in order to be exposed. They must _also_ still have a Volume referring to the Listener. + #[serde(default)] + pub extra_pod_selector_labels: BTreeMap, + + /// Ports that should be exposed. + pub ports: Option>, + + /// Whether incoming traffic should also be directed to Pods that are not `Ready`. + #[serde(default = "ListenerSpec::default_publish_not_ready_addresses")] + pub publish_not_ready_addresses: Option, + } + + /// Informs users about Listeners that are bound by a given Pod. + /// + /// This is not expected to be created or modified by users. It will be created by + /// the Stackable Listener Operator when mounting the listener volume, and is always + /// named `pod-{pod.metadata.uid}`. + #[versioned(k8s( + group = "listeners.stackable.tech", + plural = "podlisteners", + namespaced, + ))] + #[derive( + CustomResource, Serialize, Deserialize, Clone, Debug, JsonSchema, Default, PartialEq, Eq, + )] + #[serde(rename_all = "camelCase")] + pub struct PodListenersSpec { + /// All Listeners currently bound by the Pod. + /// + /// Indexed by Volume name (not PersistentVolume or PersistentVolumeClaim). + pub listeners: BTreeMap, + } + + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + pub struct ListenerPort { + /// The name of the port. + /// + /// The name of each port *must* be unique within a single Listener. + pub name: String, + + /// The port number. + pub port: i32, + + // FIXME (@Techassi): Turn this into an enum + /// The layer-4 protocol (`TCP` or `UDP`). + pub protocol: Option, + } + + /// Informs users about how to reach the Listener. + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + pub struct ListenerStatus { + /// The backing Kubernetes Service. + pub service_name: Option, + + /// All addresses that the Listener is currently reachable from. + pub ingress_addresses: Option>, + + /// Port mappings for accessing the Listener on each Node that the Pods are currently running on. + /// + /// This is only intended for internal use by listener-operator itself. This will be left unset if using a ListenerClass that does + /// not require Node-local access. + pub node_ports: Option>, + } + + /// One address that a Listener is accessible from. + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + pub struct ListenerIngress { + /// The hostname or IP address to the Listener. + pub address: String, + + /// The type of address (`Hostname` or `IP`). + pub address_type: core_v1alpha1::AddressType, + + /// Port mapping table. + pub ports: BTreeMap, + } + + #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + pub struct PodListener { + /// `Node` if this address only allows access to Pods hosted on a specific Kubernetes Node, otherwise `Cluster`. + pub scope: PodListenerScope, + + /// Addresses allowing access to this Pod. + /// + /// Compared to `ingress_addresses` on the Listener status, this list is restricted to addresses that can access this Pod. + /// + /// This field is intended to be equivalent to the files mounted into the Listener volume. + pub ingress_addresses: Option>, + } + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "PascalCase")] + pub enum PodListenerScope { + Node, + Cluster, + } +} diff --git a/crates/stackable-operator/src/crd/listener/listeners/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/listener/listeners/v1alpha1_impl.rs new file mode 100644 index 000000000..b9351cf32 --- /dev/null +++ b/crates/stackable-operator/src/crd/listener/listeners/v1alpha1_impl.rs @@ -0,0 +1,7 @@ +use crate::crd::listener::listeners::v1alpha1::ListenerSpec; + +impl ListenerSpec { + pub(super) const fn default_publish_not_ready_addresses() -> Option { + Some(true) + } +} diff --git a/crates/stackable-operator/src/crd/listener/mod.rs b/crates/stackable-operator/src/crd/listener/mod.rs index 55ff3419b..7f45a88a1 100644 --- a/crates/stackable-operator/src/crd/listener/mod.rs +++ b/crates/stackable-operator/src/crd/listener/mod.rs @@ -1,118 +1,18 @@ //! This modules provides resource types used to interact with [listener-operator][listener-docs]. //! -//! # Custom Resources -//! -//! ## [`Listener`] -//! -//! Exposes a set of pods, either internally to the cluster or to the outside world. The mechanism -//! for how it is exposed is managed by the [`ListenerClass`]. -//! -//! It can be either created manually by the application administrator (for applications that expose -//! a single load-balanced endpoint), or automatically when mounting a [listener volume][lvb] (for -//! applications that expose a separate endpoint per replica). -//! -//! All exposed pods *must* have a mounted [listener volume][lvb], regardless of whether the -//! [`Listener`] is created automatically. -//! -//! ## [`ListenerClass`] -//! -//! Declares a policy for how [`Listener`]s are exposed to users. -//! -//! It is created by the cluster administrator. -//! -//! ## [`PodListeners`] -//! -//! Informs users and other operators about the state of all [`Listener`]s associated with a [`Pod`]. -//! -//! It is created by the Stackable Secret Operator, and always named `pod-{pod.metadata.uid}`. -//! //! [listener-docs]: https://docs.stackable.tech/listener-operator/stable/index.html //! [lvb]: ListenerOperatorVolumeSourceBuilder #[cfg(doc)] use k8s_openapi::api::core::v1::{Node, PersistentVolume, PersistentVolumeClaim, Pod, Volume}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; #[cfg(doc)] use crate::builder::pod::volume::ListenerOperatorVolumeSourceBuilder; mod class; +mod core; mod listeners; -pub use class::*; -pub use listeners::*; - -/// The method used to access the services. -// -// Please note that this does not necessarily need to be restricted to the same Service types Kubernetes supports. -// Listeners currently happens to support the same set of service types as upstream Kubernetes, but we still want to -// have the freedom to add custom ones in the future (for example: Istio ingress?). -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -pub enum ServiceType { - /// Reserve a port on each node. - NodePort, - - /// Provision a dedicated load balancer. - LoadBalancer, - - /// Assigns an IP address from a pool of IP addresses that your cluster has reserved for that purpose. - ClusterIP, -} - -/// Service Internal Traffic Policy enables internal traffic restrictions to only route internal traffic to endpoints -/// within the node the traffic originated from. The "internal" traffic here refers to traffic originated from Pods in -/// the current cluster. This can help to reduce costs and improve performance. -/// See [Kubernetes docs](https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/). -// -// Please note that this represents a Kubernetes type, so the name of the enum variant needs to exactly match the -// Kubernetes traffic policy. -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq, Eq, strum::Display)] -pub enum KubernetesTrafficPolicy { - /// Obscures the client source IP and may cause a second hop to another node, but allows Kubernetes to spread the load between all nodes. - Cluster, - - /// Preserves the client source IP and avoid a second hop for LoadBalancer and NodePort type Services, but makes clients responsible for spreading the load. - Local, -} - -/// The type of a given address. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "PascalCase")] -pub enum AddressType { - /// A resolvable DNS hostname. - Hostname, - - /// A resolved IP address. - #[serde(rename = "IP")] - Ip, -} - -/// A mode for deciding the preferred [`AddressType`]. -/// -/// These can vary depending on the rest of the [`ListenerClass`]. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, JsonSchema, PartialEq, Eq)] -pub enum PreferredAddressType { - /// Like [`AddressType::Hostname`], but prefers [`AddressType::Ip`] for [`ServiceType::NodePort`], since their hostnames are less likely to be resolvable. - HostnameConservative, - - // Like the respective variants of AddressType. Ideally we would refer to them instead of copy/pasting, but that breaks due to upstream issues: - // - https://github.com/GREsau/schemars/issues/222 - // - https://github.com/kube-rs/kube/issues/1622 - Hostname, - #[serde(rename = "IP")] - Ip, -} - -impl PreferredAddressType { - pub fn resolve(self, listener_class: &ListenerClassSpec) -> AddressType { - match self { - PreferredAddressType::HostnameConservative => match listener_class.service_type { - ServiceType::NodePort => AddressType::Ip, - _ => AddressType::Hostname, - }, - PreferredAddressType::Hostname => AddressType::Hostname, - PreferredAddressType::Ip => AddressType::Ip, - } - } +pub mod v1alpha1 { + pub use super::{class::v1alpha1::*, core::v1alpha1::*, listeners::v1alpha1::*}; } From 8dc49bd4d1fb0f04052f5fdf87076e7df9f4d789 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 16:46:55 +0200 Subject: [PATCH 17/22] test: Fix authentication doc test --- crates/stackable-operator/src/crd/authentication/core/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index eaf64f721..c7c65a6f8 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -109,7 +109,7 @@ pub mod versioned { /// ``` /// # use schemars::JsonSchema; /// # use serde::{Deserialize, Serialize}; - /// use stackable_operator::crd::authentication::ClientAuthenticationDetails; + /// use stackable_operator::crd::authentication::core::v1alpha1; /// /// #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] /// #[serde(rename_all = "camelCase")] @@ -118,7 +118,7 @@ pub mod versioned { /// pub user_registration: bool, /// /// #[serde(flatten)] - /// pub common: ClientAuthenticationDetails, + /// pub common: v1alpha1::ClientAuthenticationDetails, /// } /// ``` #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] From c6d8c4801a93d4b1b661288002dcfa0fb403f6b0 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 17:06:55 +0200 Subject: [PATCH 18/22] chore: Add changelog entry --- crates/stackable-operator/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index d674fc2a7..d1971a0b1 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -8,6 +8,15 @@ All notable changes to this project will be documented in this file. - Add Deployments to `ClusterResource`s ([#992]). +### Changed + +- BREAKING: Version common CRD structs and enums ([#968]). + - All CRD-related types and function now reside in the `stackable_operator::crd` module. + - Each CRD-related struct and enum has been versioned. The initial version is `v1alpha1`. + - The `static` authentication provider must now be imported using `r#static`. + - Import are now more granular in general. + +[#968]: https://github.com/stackabletech/operator-rs/pull/968 [#992]: https://github.com/stackabletech/operator-rs/pull/992 ## [0.87.5] - 2025-03-19 From 4969c7d3d2c12a004e821442c1ba9d1623522cd0 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 1 Apr 2025 11:14:16 +0200 Subject: [PATCH 19/22] chore: Adjust imports --- .../src/crd/authentication/core/mod.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index c7c65a6f8..49c6f40c7 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -10,11 +10,7 @@ pub mod versioned { // This makes v1alpha1 versions of all authentication providers available to the // AuthenticationClassProvider enum below. mod v1alpha1 { - use crate::crd::authentication::{ - kerberos::v1alpha1 as kerberos_v1alpha1, ldap::v1alpha1 as ldap_v1alpha1, - oidc::v1alpha1 as oidc_v1alpha1, r#static::v1alpha1 as static_v1alpha1, - tls::v1alpha1 as tls_v1alpha1, - }; + use crate::crd::authentication::{kerberos, ldap, oidc, r#static, tls}; } /// The Stackable Platform uses the AuthenticationClass as a central mechanism to handle user /// authentication across supported products. @@ -72,23 +68,23 @@ pub mod versioned { pub enum AuthenticationClassProvider { /// The [static provider](https://DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_static) /// is used to configure a static set of users, identified by username and password. - Static(static_v1alpha1::AuthenticationProvider), + Static(r#static::v1alpha1::AuthenticationProvider), /// The [LDAP provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_ldap). /// There is also the ["Authentication with LDAP" tutorial](DOCS_BASE_URL_PLACEHOLDER/tutorials/authentication_with_openldap) /// where you can learn to configure Superset and Trino with OpenLDAP. - Ldap(ldap_v1alpha1::AuthenticationProvider), + Ldap(ldap::v1alpha1::AuthenticationProvider), /// The OIDC provider can be used to configure OpenID Connect. - Oidc(oidc_v1alpha1::AuthenticationProvider), + Oidc(oidc::v1alpha1::AuthenticationProvider), /// The [TLS provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_tls). /// The TLS AuthenticationClass is used when users should authenticate themselves with a TLS certificate. - Tls(tls_v1alpha1::AuthenticationProvider), + Tls(tls::v1alpha1::AuthenticationProvider), /// The [Kerberos provider](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication#_kerberos). /// The Kerberos AuthenticationClass is used when users should authenticate themselves via Kerberos. - Kerberos(kerberos_v1alpha1::AuthenticationProvider), + Kerberos(kerberos::v1alpha1::AuthenticationProvider), } /// Common [`v1alpha1::ClientAuthenticationDetails`] which is specified at the client/ product @@ -140,6 +136,6 @@ pub mod versioned { // that user can not configure multiple options at the same time (yes we are aware that this makes a // changing the type of an AuthenticationClass harder). // This is a non-breaking change though :) - oidc: Option>, + oidc: Option>, } } From b0ed7d85ac777fdbb886ee05b965a11fa76ea7f5 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 1 Apr 2025 11:15:46 +0200 Subject: [PATCH 20/22] chore: Apply suggestion Co-authored-by: Nick <10092581+NickLarsenNZ@users.noreply.github.com> --- crates/stackable-operator/src/crd/listener/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/stackable-operator/src/crd/listener/mod.rs b/crates/stackable-operator/src/crd/listener/mod.rs index 7f45a88a1..398ef37e7 100644 --- a/crates/stackable-operator/src/crd/listener/mod.rs +++ b/crates/stackable-operator/src/crd/listener/mod.rs @@ -13,6 +13,7 @@ mod class; mod core; mod listeners; +// Group all v1alpha1 items in one module. pub mod v1alpha1 { pub use super::{class::v1alpha1::*, core::v1alpha1::*, listeners::v1alpha1::*}; } From 5f4030206716404f5469f9f46ea49050d8a3bb47 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 2 Apr 2025 13:14:54 +0200 Subject: [PATCH 21/22] chore: Adjust imports --- crates/stackable-operator/src/cluster_resources.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/stackable-operator/src/cluster_resources.rs b/crates/stackable-operator/src/cluster_resources.rs index c69cc326c..73d63269a 100644 --- a/crates/stackable-operator/src/cluster_resources.rs +++ b/crates/stackable-operator/src/cluster_resources.rs @@ -37,7 +37,7 @@ use crate::{ ResourceRequirementsExt, ResourceRequirementsType, }, }, - crd::listener::v1alpha1 as listener_v1alpha1, + crd::listener, kvp::{ Label, LabelError, Labels, consts::{K8S_APP_INSTANCE_KEY, K8S_APP_MANAGED_BY_KEY, K8S_APP_NAME_KEY}, @@ -205,7 +205,7 @@ impl ClusterResource for Service {} impl ClusterResource for ServiceAccount {} impl ClusterResource for RoleBinding {} impl ClusterResource for PodDisruptionBudget {} -impl ClusterResource for listener_v1alpha1::Listener {} +impl ClusterResource for listener::v1alpha1::Listener {} impl ClusterResource for Job { fn pod_spec(&self) -> Option<&PodSpec> { @@ -646,7 +646,7 @@ impl ClusterResources { self.delete_orphaned_resources_of_kind::(client), self.delete_orphaned_resources_of_kind::(client), self.delete_orphaned_resources_of_kind::(client), - self.delete_orphaned_resources_of_kind::(client), + self.delete_orphaned_resources_of_kind::(client), )?; Ok(()) From 9f4822f4f25a2f8b1c30726ac2ff0b0f8f1ec7e0 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 17 Apr 2025 09:30:22 +0200 Subject: [PATCH 22/22] docs: Fix doc comment reference --- crates/stackable-operator/src/crd/authentication/core/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/crd/authentication/core/mod.rs b/crates/stackable-operator/src/crd/authentication/core/mod.rs index 49c6f40c7..35ec3dca1 100644 --- a/crates/stackable-operator/src/crd/authentication/core/mod.rs +++ b/crates/stackable-operator/src/crd/authentication/core/mod.rs @@ -96,7 +96,7 @@ pub mod versioned { /// to wrap this struct and use `#[serde(flatten)]` on the field. /// /// Additionally, it might be the case that special fields are needed in the contained structs, - /// such as [`oidc_v1alpha1::ClientAuthenticationOptions`]. To be able to add custom fields in + /// such as [`oidc::v1alpha1::ClientAuthenticationOptions`]. To be able to add custom fields in /// that structs without serde(flattening) multiple structs, they are generic, so you can add /// additional attributes if needed. ///