diff --git a/Cargo.lock b/Cargo.lock index e3b159d5..b760a69c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1822,6 +1822,7 @@ dependencies = [ name = "stackable-druid-crd" version = "0.9.0-nightly" dependencies = [ + "indoc", "lazy_static", "rstest", "semver", diff --git a/deploy/crd/druidcluster.crd.yaml b/deploy/crd/druidcluster.crd.yaml index 2b9620b9..7c7dd8c8 100644 --- a/deploy/crd/druidcluster.crd.yaml +++ b/deploy/crd/druidcluster.crd.yaml @@ -23,6 +23,7 @@ spec: spec: properties: brokers: + description: Configuration of the broker role properties: cliOverrides: additionalProperties: @@ -355,22 +356,27 @@ spec: - roleGroups type: object clusterConfig: - description: Common cluster wide configuration that can not differ or be overriden on a role or role group level + description: Common cluster wide configuration that can not differ or be overridden on a role or role group level properties: authentication: - description: Authentication class settings for Druid like TLS authentication or LDAP - nullable: true - properties: - tls: - description: TLS based client authentication (mutual TLS) - nullable: true - properties: - authenticationClass: - type: string - required: - - authenticationClass - type: object - type: object + default: [] + description: List of Authentication classes using like TLS or LDAP to authenticate users + items: + properties: + authenticationClass: + description: |- + The AuthenticationClass to use. + + ## TLS provider + + Only affects client connections. This setting controls: - If clients need to authenticate themselves against Druid via TLS - Which ca.crt to use when validating the provided client certs + + Please note that the SecretClass used to authenticate users needs to be the same as the SecretClass used for internal communication. + type: string + required: + - authenticationClass + type: object + type: array authorization: description: Authorization settings for Druid like OPA nullable: true @@ -667,15 +673,15 @@ spec: type: object tls: default: - secretClass: tls - description: TLS encryption settings for Druid + serverAndInternalSecretClass: tls + description: TLS encryption settings for Druid. This setting only affects server and internal communication. It does not affect client tls authentication, use `clusterConfig.authentication` instead. nullable: true properties: - secretClass: - description: 'Only affects client connections. This setting controls: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the client Important: This will activate encrypted internal druid communication as well!' + serverAndInternalSecretClass: + default: tls + description: 'This setting controls client as well as internal tls usage: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the clients - Which cert the servers should use to authenticate themselves among each other' + nullable: true type: string - required: - - secretClass type: object zookeeperConfigMapName: description: ZooKeeper discovery ConfigMap @@ -686,6 +692,7 @@ spec: - zookeeperConfigMapName type: object coordinators: + description: Configuration of the coordinator role properties: cliOverrides: additionalProperties: @@ -1018,6 +1025,7 @@ spec: - roleGroups type: object historicals: + description: Configuration of the historical role properties: cliOverrides: additionalProperties: @@ -1507,6 +1515,7 @@ spec: type: string type: object middleManagers: + description: Configuration of the middle managed role properties: cliOverrides: additionalProperties: @@ -1839,6 +1848,7 @@ spec: - roleGroups type: object routers: + description: Configuration of the router role properties: cliOverrides: additionalProperties: diff --git a/deploy/helm/druid-operator/crds/crds.yaml b/deploy/helm/druid-operator/crds/crds.yaml index d22e5f05..236ec3d8 100644 --- a/deploy/helm/druid-operator/crds/crds.yaml +++ b/deploy/helm/druid-operator/crds/crds.yaml @@ -25,6 +25,7 @@ spec: spec: properties: brokers: + description: Configuration of the broker role properties: cliOverrides: additionalProperties: @@ -357,22 +358,27 @@ spec: - roleGroups type: object clusterConfig: - description: Common cluster wide configuration that can not differ or be overriden on a role or role group level + description: Common cluster wide configuration that can not differ or be overridden on a role or role group level properties: authentication: - description: Authentication class settings for Druid like TLS authentication or LDAP - nullable: true - properties: - tls: - description: TLS based client authentication (mutual TLS) - nullable: true - properties: - authenticationClass: - type: string - required: - - authenticationClass - type: object - type: object + default: [] + description: List of Authentication classes using like TLS or LDAP to authenticate users + items: + properties: + authenticationClass: + description: |- + The AuthenticationClass to use. + + ## TLS provider + + Only affects client connections. This setting controls: - If clients need to authenticate themselves against Druid via TLS - Which ca.crt to use when validating the provided client certs + + Please note that the SecretClass used to authenticate users needs to be the same as the SecretClass used for internal communication. + type: string + required: + - authenticationClass + type: object + type: array authorization: description: Authorization settings for Druid like OPA nullable: true @@ -669,15 +675,15 @@ spec: type: object tls: default: - secretClass: tls - description: TLS encryption settings for Druid + serverAndInternalSecretClass: tls + description: TLS encryption settings for Druid. This setting only affects server and internal communication. It does not affect client tls authentication, use `clusterConfig.authentication` instead. nullable: true properties: - secretClass: - description: 'Only affects client connections. This setting controls: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the client Important: This will activate encrypted internal druid communication as well!' + serverAndInternalSecretClass: + default: tls + description: 'This setting controls client as well as internal tls usage: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the clients - Which cert the servers should use to authenticate themselves among each other' + nullable: true type: string - required: - - secretClass type: object zookeeperConfigMapName: description: ZooKeeper discovery ConfigMap @@ -688,6 +694,7 @@ spec: - zookeeperConfigMapName type: object coordinators: + description: Configuration of the coordinator role properties: cliOverrides: additionalProperties: @@ -1020,6 +1027,7 @@ spec: - roleGroups type: object historicals: + description: Configuration of the historical role properties: cliOverrides: additionalProperties: @@ -1509,6 +1517,7 @@ spec: type: string type: object middleManagers: + description: Configuration of the middle managed role properties: cliOverrides: additionalProperties: @@ -1841,6 +1850,7 @@ spec: - roleGroups type: object routers: + description: Configuration of the router role properties: cliOverrides: additionalProperties: diff --git a/deploy/manifests/crds.yaml b/deploy/manifests/crds.yaml index dd2fb981..d594eece 100644 --- a/deploy/manifests/crds.yaml +++ b/deploy/manifests/crds.yaml @@ -26,6 +26,7 @@ spec: spec: properties: brokers: + description: Configuration of the broker role properties: cliOverrides: additionalProperties: @@ -358,22 +359,27 @@ spec: - roleGroups type: object clusterConfig: - description: Common cluster wide configuration that can not differ or be overriden on a role or role group level + description: Common cluster wide configuration that can not differ or be overridden on a role or role group level properties: authentication: - description: Authentication class settings for Druid like TLS authentication or LDAP - nullable: true - properties: - tls: - description: TLS based client authentication (mutual TLS) - nullable: true - properties: - authenticationClass: - type: string - required: - - authenticationClass - type: object - type: object + default: [] + description: List of Authentication classes using like TLS or LDAP to authenticate users + items: + properties: + authenticationClass: + description: |- + The AuthenticationClass to use. + + ## TLS provider + + Only affects client connections. This setting controls: - If clients need to authenticate themselves against Druid via TLS - Which ca.crt to use when validating the provided client certs + + Please note that the SecretClass used to authenticate users needs to be the same as the SecretClass used for internal communication. + type: string + required: + - authenticationClass + type: object + type: array authorization: description: Authorization settings for Druid like OPA nullable: true @@ -670,15 +676,15 @@ spec: type: object tls: default: - secretClass: tls - description: TLS encryption settings for Druid + serverAndInternalSecretClass: tls + description: TLS encryption settings for Druid. This setting only affects server and internal communication. It does not affect client tls authentication, use `clusterConfig.authentication` instead. nullable: true properties: - secretClass: - description: 'Only affects client connections. This setting controls: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the client Important: This will activate encrypted internal druid communication as well!' + serverAndInternalSecretClass: + default: tls + description: 'This setting controls client as well as internal tls usage: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the clients - Which cert the servers should use to authenticate themselves among each other' + nullable: true type: string - required: - - secretClass type: object zookeeperConfigMapName: description: ZooKeeper discovery ConfigMap @@ -689,6 +695,7 @@ spec: - zookeeperConfigMapName type: object coordinators: + description: Configuration of the coordinator role properties: cliOverrides: additionalProperties: @@ -1021,6 +1028,7 @@ spec: - roleGroups type: object historicals: + description: Configuration of the historical role properties: cliOverrides: additionalProperties: @@ -1510,6 +1518,7 @@ spec: type: string type: object middleManagers: + description: Configuration of the middle managed role properties: cliOverrides: additionalProperties: @@ -1842,6 +1851,7 @@ spec: - roleGroups type: object routers: + description: Configuration of the router role properties: cliOverrides: additionalProperties: diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index f5b62bac..c394bb6f 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -265,11 +265,11 @@ TLS encryption is supported for internal cluster communication (e.g. between Bro spec: clusterConfig: tls: - secretClass: tls # <1> + serverAndInternalSecretClass: tls # <1> ---- <1> Name of the `SecretClass` that is used to encrypt internal and external communication. -IMPORTANT: A Stackable Druid cluster is always encrypted per default. In order to disable this default behavior you can set `spec.clusterConfig.tls: null`. +IMPORTANT: A Stackable Druid cluster is always encrypted per default. In order to disable this default behavior you can set `spec.clusterConfig.tls.serverAndInternalSecretClass: null`. === Authentication @@ -282,13 +282,10 @@ The access to the Druid cluster can be limited by configuring client authenticat spec: clusterConfig: authentication: - tls: - authenticationClass: druid-tls-auth # <1> + - authenticationClass: druid-tls-auth # <1> ---- <1> Name of the `AuthenticationClass` that is used to encrypt and authenticate communication. -IMPORTANT: The TLS `AuthenticationClass` and its respective `SecretClass` will always take precedence over the TLS encryption `SecretClass` (if provided). - The `AuthenticationClass` may or may not have a `SecretClass` configured: [source,yaml] ---- @@ -303,10 +300,10 @@ spec: tls: clientCertSecretClass: druid-mtls # <1> # Option 2 - tls: null # <2> + tls: {} # <2> ---- <1> If a client `SecretClass` is provided in the `AuthenticationClass` (here `druid-mtls`), these certificates will be used for encryption and authentication. -<2> If no client `SecretClass` is provided in the `AuthenticationClass`, the `spec.clusterConfig.tls.secretClass` will be used for encryption and authentication. It cannot be explicitly set to null in this case. +<2> If no client `SecretClass` is provided in the `AuthenticationClass`, the `spec.clusterConfig.tls.serverAndInternalSecretClass` will be used for encryption and authentication. It cannot be explicitly set to null in this case. ==== LDAP diff --git a/examples/tls/tls-druid-cluster.yaml b/examples/tls/tls-druid-cluster.yaml index ea10e490..e1f4c7d5 100644 --- a/examples/tls/tls-druid-cluster.yaml +++ b/examples/tls/tls-druid-cluster.yaml @@ -83,8 +83,7 @@ spec: stackableVersion: 0.3.0 clusterConfig: authentication: - tls: - authenticationClass: druid-mtls-authentication-class + - authenticationClass: druid-mtls-authentication-class deepStorage: hdfs: configMapName: druid-hdfs @@ -95,7 +94,7 @@ spec: host: localhost port: 1527 tls: - secretClass: tls + serverAndInternalSecretClass: tls zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/rust/crd/Cargo.toml b/rust/crd/Cargo.toml index edadff3d..ecce9756 100644 --- a/rust/crd/Cargo.toml +++ b/rust/crd/Cargo.toml @@ -20,5 +20,6 @@ snafu = "0.7" lazy_static = "1.4" [dev-dependencies] -serde_yaml = "0.8" +indoc = "1.0" rstest = "0.16" +serde_yaml = "0.8" diff --git a/rust/crd/src/authentication.rs b/rust/crd/src/authentication.rs index df5916fc..deaab60b 100644 --- a/rust/crd/src/authentication.rs +++ b/rust/crd/src/authentication.rs @@ -1,95 +1,318 @@ -use crate::DruidCluster; - use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; use stackable_operator::{ client::Client, - commons::{ - authentication::{AuthenticationClass, AuthenticationClassProvider}, - tls::TlsAuthenticationProvider, - }, + commons::authentication::{AuthenticationClass, AuthenticationClassProvider}, kube::runtime::reflector::ObjectRef, schemars::{self, JsonSchema}, }; -use strum::{EnumDiscriminants, IntoStaticStr}; -#[derive(Snafu, Debug, EnumDiscriminants)] -#[strum_discriminants(derive(IntoStaticStr))] +use crate::DruidCluster; + +const SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS: [&str; 1] = ["TLS"]; + +#[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("Failed to retrieve AuthenticationClass [{authentication_class}]"))] + #[snafu(display("failed to retrieve AuthenticationClass [{authentication_class}]"))] AuthenticationClassRetrieval { source: stackable_operator::error::Error, authentication_class: ObjectRef, }, - #[snafu(display("The Druid operator doesn't support the AuthenticationClass provider [{authentication_class_provider}] from AuthenticationClass [{authentication_class}]"))] - AuthenticationClassProviderNotSupported { - authentication_class_provider: String, + // TODO: Adapt message if multiple authentication classes are supported + #[snafu(display("only one authentication class is currently supported. Possible Authentication class providers are {SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS:?}"))] + MultipleAuthenticationClassesProvided, + #[snafu(display( + "failed to use authentication provider [{provider}] for authentication class [{authentication_class}] - supported providers: {SUPPORTED_AUTHENTICATION_CLASS_PROVIDERS:?}", + ))] + AuthenticationProviderNotSupported { + authentication_class: ObjectRef, + provider: String, + }, + #[snafu(display( + "client authentication using TLS (as requested by AuthenticationClass {authentication_class}) can not be used when Druid server and internal TLS is disabled", + ))] + TlsAuthenticationClassWithoutDruidServerTls { + authentication_class: ObjectRef, + }, + #[snafu(display( + "client authentication using TLS (as requested by AuthenticationClass {authentication_class}) can only use the same SecretClass as the Druid instance is using for server and internal communication (SecretClass {server_and_internal_secret_class} in this case)", + ))] + TlsAuthenticationClassSecretClassDiffersFromDruidServerTls { authentication_class: ObjectRef, + server_and_internal_secret_class: String, }, } -#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +#[derive(Clone, Deserialize, Debug, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DruidAuthentication { - /// TLS based client authentication (mutual TLS) - pub tls: Option, + /// The AuthenticationClass to use. + /// + /// ## TLS provider + /// + /// Only affects client connections. This setting controls: + /// - If clients need to authenticate themselves against Druid via TLS + /// - Which ca.crt to use when validating the provided client certs + /// + /// Please note that the SecretClass used to authenticate users needs to be the same + /// as the SecretClass used for internal communication. + pub authentication_class: String, } -#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DruidTlsAuthentication { - pub authentication_class: String, +#[derive(Clone, Debug)] +/// Helper struct that contains resolved AuthenticationClasses to reduce network API calls. +pub struct ResolvedAuthenticationClasses { + resolved_authentication_classes: Vec, } -impl DruidAuthentication { - pub async fn resolve( +impl ResolvedAuthenticationClasses { + pub fn new(resolved_authentication_classes: Vec) -> Self { + Self { + resolved_authentication_classes, + } + } + + /// Resolve provided AuthenticationClasses via API calls and validate the contents. + /// Currently errors out if: + /// - AuthenticationClass could not be resolved + /// - Validation failed + pub async fn from_references( client: &Client, druid: &DruidCluster, - ) -> Result, Error> { - let mut druid_authentication_config: Vec = vec![]; - - if let Some(DruidAuthentication { - tls: Some(druid_tls), - }) = &druid.spec.cluster_config.authentication - { - let authentication_class = - AuthenticationClass::resolve(client, &druid_tls.authentication_class) + auth_classes: &Vec, + ) -> Result { + let mut resolved_authentication_classes: Vec = vec![]; + + for auth_class in auth_classes { + resolved_authentication_classes.push( + AuthenticationClass::resolve(client, &auth_class.authentication_class) .await .context(AuthenticationClassRetrievalSnafu { authentication_class: ObjectRef::::new( - &druid_tls.authentication_class, + &auth_class.authentication_class, ), - })?; + })?, + ); + } - match authentication_class.spec.provider { - AuthenticationClassProvider::Tls(tls_provider) => { - druid_authentication_config.push(DruidAuthenticationConfig::Tls(tls_provider)); - } + ResolvedAuthenticationClasses::new(resolved_authentication_classes).validate( + druid + .spec + .cluster_config + .tls + .as_ref() + .and_then(|tls| tls.server_and_internal_secret_class.clone()), + ) + } + + /// Return the (first) TLS `AuthenticationClass` if available + pub fn get_tls_authentication_class(&self) -> Option<&AuthenticationClass> { + self.resolved_authentication_classes + .iter() + .find(|auth| matches!(auth.spec.provider, AuthenticationClassProvider::Tls(_))) + } + + /// Validates the resolved AuthenticationClasses. + /// Currently errors out if: + /// - More than one AuthenticationClass was provided + /// - AuthenticationClass provider was not supported + pub fn validate(self, server_and_internal_secret_class: Option) -> Result { + if self.resolved_authentication_classes.len() > 1 { + return Err(Error::MultipleAuthenticationClassesProvided); + } + + for auth_class in &self.resolved_authentication_classes { + match &auth_class.spec.provider { + AuthenticationClassProvider::Tls(_) => {} _ => { - return Err(Error::AuthenticationClassProviderNotSupported { - authentication_class_provider: authentication_class - .spec - .provider - .to_string(), - authentication_class: ObjectRef::::new( - &druid_tls.authentication_class, - ), + return Err(Error::AuthenticationProviderNotSupported { + authentication_class: ObjectRef::from_obj(auth_class), + provider: auth_class.spec.provider.to_string(), }) } } } - Ok(druid_authentication_config) + if let Some(tls_auth_class) = self.get_tls_authentication_class() { + match &server_and_internal_secret_class { + Some(server_and_internal_secret_class) => { + // Check that the tls AuthenticationClass uses the same SecretClass as the Druid server itself + match &tls_auth_class.spec.provider { + AuthenticationClassProvider::Tls(tls) => { + if let Some(auth_class_secret_class) = &tls.client_cert_secret_class { + if auth_class_secret_class != server_and_internal_secret_class { + return Err(Error::TlsAuthenticationClassSecretClassDiffersFromDruidServerTls { authentication_class: ObjectRef::from_obj(tls_auth_class), server_and_internal_secret_class: server_and_internal_secret_class.clone() }); + } + } + } + _ => unreachable!( + "We know for sure we can only have tls AuthenticationClasses here" + ), + } + } + None => { + // Check that no tls AuthenticationClass is used when Druid server_and_internal tls is disabled + return Err(Error::TlsAuthenticationClassWithoutDruidServerTls { + authentication_class: ObjectRef::from_obj(tls_auth_class), + }); + } + } + } + + Ok(self) } } -#[derive(Clone, Debug)] -pub enum DruidAuthenticationConfig { - Tls(TlsAuthenticationProvider), -} +#[cfg(test)] +mod tests { + use crate::authentication::{Error, ResolvedAuthenticationClasses}; + use stackable_operator::{commons::authentication::AuthenticationClass, kube::ResourceExt}; + + #[test] + fn test_authentication_classes_validation() { + let classes = ResolvedAuthenticationClasses::new(vec![]); + assert!( + classes.validate(None).is_ok(), + "Supported: No server tls, no AuthenticationClasses" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![ + get_tls_authentication_class_without_secret_class(), + ]); + assert!( + matches!( + classes.validate(None), + Err(Error::TlsAuthenticationClassWithoutDruidServerTls { authentication_class }) if authentication_class.name == "tls", + ), + "Not supported: No server tls, TLS authentication class" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![get_ldap_authentication_class()]); + assert!( + matches!( + classes.validate(None), + Err(Error::AuthenticationProviderNotSupported { provider, authentication_class }) if provider == "Ldap" && authentication_class.name == "ldap", + ), + "Not supported: No server tls, LDAP authentication class" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![]); + assert!( + classes.validate(Some("tls".to_string())).is_ok(), + "Supported: Server tls, no AuthenticationClasses" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![ + get_tls_authentication_class_without_secret_class(), + ]); + assert!( + classes.validate(Some("tls".to_string())).is_ok(), + "Supported: Server tls, TLS authentication class without SecretClass" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![ + get_tls_authentication_class_with_secret_class_tls(), + ]); + assert!( + classes.validate(Some("tls".to_string())).is_ok(), + "Supported: Server tls, TLS authentication class with same SecretClass as Druid cluster" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![ + get_tls_authentication_class_with_secret_class_druid_clients(), + ]); + assert!( + matches!( + classes.validate(Some("tls-druid".to_string())), + Err(Error::TlsAuthenticationClassSecretClassDiffersFromDruidServerTls{ authentication_class, server_and_internal_secret_class }) if authentication_class.name == "tls-druid-clients" && server_and_internal_secret_class == "tls-druid" + ), + "Not supported: Server tls, TLS authentication class with *different* SecretClass as Druid cluster" + ); + + let classes = ResolvedAuthenticationClasses::new(vec![ + get_tls_authentication_class_without_secret_class(), + get_tls_authentication_class_without_secret_class(), + ]); + assert!( + matches!( + classes.validate(Some("tls-druid".to_string())), + Err(Error::MultipleAuthenticationClassesProvided {}) + ), + "Not supported: Server tls, multiple authentication classes" + ); + } + + #[test] + fn test_get_tls_authentication_class() { + let classes = ResolvedAuthenticationClasses::new(vec![ + get_ldap_authentication_class(), + get_tls_authentication_class_without_secret_class(), + get_tls_authentication_class_with_secret_class_druid_clients(), + ]); + + let tls_authentication_class = classes.get_tls_authentication_class(); + + // TODO Check deriving PartialEq for AuthenticationClass so that we can compare them directly instead of comparing the names + assert_eq!( + tls_authentication_class.map(|class| class.name_unchecked()), + Some("tls".to_string()) + ); + } + + fn get_tls_authentication_class_without_secret_class() -> AuthenticationClass { + let input = r#" +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: tls +spec: + provider: + tls: {} +"#; + serde_yaml::from_str(input).expect("illegal test input") + } + + fn get_tls_authentication_class_with_secret_class_tls() -> AuthenticationClass { + let input = r#" +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: tls-tls +spec: + provider: + tls: + clientCertSecretClass: tls +"#; + serde_yaml::from_str(input).expect("illegal test input") + } + + fn get_tls_authentication_class_with_secret_class_druid_clients() -> AuthenticationClass { + let input = r#" +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: tls-druid-clients +spec: + provider: + tls: + clientCertSecretClass: druid-clients +"#; + serde_yaml::from_str(input).expect("illegal test input") + } -impl DruidAuthenticationConfig { - pub fn is_tls_auth(&self) -> bool { - matches!(self, DruidAuthenticationConfig::Tls(_)) + fn get_ldap_authentication_class() -> AuthenticationClass { + let input = r#" +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: ldap +spec: + provider: + ldap: + hostname: my.ldap.server + port: 389 + searchBase: ou=users,dc=example,dc=org +"#; + serde_yaml::from_str(input).expect("illegal test input") } } diff --git a/rust/crd/src/authorization.rs b/rust/crd/src/authorization.rs new file mode 100644 index 00000000..f7f61d14 --- /dev/null +++ b/rust/crd/src/authorization.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use stackable_operator::{ + commons::opa::OpaConfig, + schemars::{self, JsonSchema}, +}; + +#[derive(Clone, Deserialize, Debug, Default, Eq, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DruidAuthorization { + pub opa: OpaConfig, +} diff --git a/rust/crd/src/lib.rs b/rust/crd/src/lib.rs index ebcf48ef..0755b8ed 100644 --- a/rust/crd/src/lib.rs +++ b/rust/crd/src/lib.rs @@ -1,17 +1,19 @@ pub mod authentication; +pub mod authorization; pub mod resource; +pub mod security; pub mod storage; pub mod tls; use crate::authentication::DruidAuthentication; use crate::tls::DruidTls; +use authorization::DruidAuthorization; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ client::Client, commons::{ - opa::OpaConfig, product_image_selection::ProductImage, resources::{NoRuntimeLimits, ResourcesFragment}, s3::{InlinedS3BucketSpec, S3BucketDef, S3ConnectionDef, S3ConnectionSpec}, @@ -30,6 +32,7 @@ use std::{ str::FromStr, }; use strum::{Display, EnumDiscriminants, EnumIter, EnumString, IntoStaticStr}; +use tls::default_druid_tls; pub const APP_NAME: &str = "druid"; pub const OPERATOR_NAME: &str = "druid.stackable.tech"; @@ -58,17 +61,7 @@ pub const PATH_SEGMENT_CACHE: &str = "/stackable/var/druid/segment-cache"; // CONFIG PROPERTIES // ///////////////////////////// // extensions -const EXTENSIONS_LOADLIST: &str = "druid.extensions.loadList"; -const EXT_S3: &str = "druid-s3-extensions"; -const EXT_KAFKA_INDEXING: &str = "druid-kafka-indexing-service"; -const EXT_DATASKETCHES: &str = "druid-datasketches"; -const PROMETHEUS_EMITTER: &str = "prometheus-emitter"; -const EXT_PSQL_MD_ST: &str = "postgresql-metadata-storage"; -const EXT_MYSQL_MD_ST: &str = "mysql-metadata-storage"; -const EXT_OPA_AUTHORIZER: &str = "druid-opa-authorizer"; -const EXT_BASIC_SECURITY: &str = "druid-basic-security"; -const EXT_HDFS: &str = "druid-hdfs-storage"; -const EXT_SIMPLE_CLIENT_SSL_CONTEXT: &str = "simple-client-sslcontext"; +pub const EXTENSIONS_LOADLIST: &str = "druid.extensions.loadList"; // zookeeper pub const ZOOKEEPER_CONNECTION_STRING: &str = "druid.zk.service.host"; // deep storage @@ -99,8 +92,6 @@ pub const CREDENTIALS_SECRET_PROPERTY: &str = "credentialsSecret"; // metrics pub const PROMETHEUS_PORT: &str = "druid.emitter.prometheus.port"; pub const METRICS_PORT: u16 = 9090; -// tls -const DEFAULT_TLS_SECRET_CLASS: &str = "tls"; // container locations pub const S3_SECRET_DIR_NAME: &str = "/stackable/secrets"; const ENV_S3_ACCESS_KEY: &str = "AWS_ACCESS_KEY_ID"; @@ -154,21 +145,26 @@ pub struct DruidClusterSpec { pub stopped: Option, /// The Druid image to use pub image: ProductImage, + /// Configuration of the broker role pub brokers: Role, + /// Configuration of the coordinator role pub coordinators: Role, + /// Configuration of the historical role pub historicals: Role, + /// Configuration of the middle managed role pub middle_managers: Role, + /// Configuration of the router role pub routers: Role, - /// Common cluster wide configuration that can not differ or be overriden on a role or role group level + /// Common cluster wide configuration that can not differ or be overridden on a role or role group level pub cluster_config: DruidClusterConfig, } #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] #[serde(rename_all = "camelCase")] pub struct DruidClusterConfig { - /// Authentication class settings for Druid like TLS authentication or LDAP - #[serde(skip_serializing_if = "Option::is_none")] - pub authentication: Option, + /// List of Authentication classes using like TLS or LDAP to authenticate users + #[serde(default)] + pub authentication: Vec, /// Authorization settings for Druid like OPA #[serde(skip_serializing_if = "Option::is_none")] pub authorization: Option, @@ -179,28 +175,15 @@ pub struct DruidClusterConfig { pub ingestion: Option, /// Meta storage database like Derby or PostgreSQL pub metadata_storage_database: DatabaseConnectionSpec, - /// TLS encryption settings for Druid - #[serde( - default = "default_tls_secret_class", - skip_serializing_if = "Option::is_none" - )] + /// TLS encryption settings for Druid. + /// This setting only affects server and internal communication. + /// It does not affect client tls authentication, use `clusterConfig.authentication` instead. + #[serde(default = "default_druid_tls", skip_serializing_if = "Option::is_none")] pub tls: Option, /// ZooKeeper discovery ConfigMap pub zookeeper_config_map_name: String, } -fn default_tls_secret_class() -> Option { - Some(DruidTls { - secret_class: DEFAULT_TLS_SECRET_CLASS.to_string(), - }) -} - -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DruidAuthorization { - pub opa: OpaConfig, -} - #[derive( Clone, Debug, @@ -320,27 +303,7 @@ impl DruidCluster { match file { JVM_CONFIG => {} RUNTIME_PROPS => { - // extensions - let mut extensions = vec![ - String::from(EXT_KAFKA_INDEXING), - String::from(EXT_DATASKETCHES), - String::from(PROMETHEUS_EMITTER), - String::from(EXT_BASIC_SECURITY), - String::from(EXT_OPA_AUTHORIZER), - String::from(EXT_HDFS), - ]; - - if self.tls_enabled() { - extensions.push(String::from(EXT_SIMPLE_CLIENT_SSL_CONTEXT)); - } - - // metadata storage - let mds = self.spec.cluster_config.metadata_storage_database.clone(); - match mds.db_type { - DbType::Derby => {} // no additional extensions required - DbType::Postgresql => extensions.push(EXT_PSQL_MD_ST.to_string()), - DbType::Mysql => extensions.push(EXT_MYSQL_MD_ST.to_string()), - } + let mds = &self.spec.cluster_config.metadata_storage_database; result.insert(MD_ST_TYPE.to_string(), Some(mds.db_type.to_string())); result.insert( MD_ST_CONNECT_URI.to_string(), @@ -354,10 +317,7 @@ impl DruidCluster { if let Some(password) = &mds.password { result.insert(MD_ST_PASSWORD.to_string(), Some(password.to_string())); } - // s3 - if self.uses_s3() { - extensions.push(EXT_S3.to_string()); - } + // OPA if let Some(DruidAuthorization { opa: _ }) = &self.spec.cluster_config.authorization { @@ -388,11 +348,7 @@ impl DruidCluster { // that is done directly in the controller } } - // other - result.insert( - EXTENSIONS_LOADLIST.to_string(), - Some(build_string_list(&extensions)), - ); + // metrics result.insert(PROMETHEUS_PORT.to_string(), Some(METRICS_PORT.to_string())); } @@ -623,20 +579,6 @@ impl DruidCluster { let s3_storage = self.spec.cluster_config.deep_storage.is_s3(); s3_ingestion || s3_storage } - - /// Determines if the cluster should be encrypted / authenticated via TLS - pub fn tls_enabled(&self) -> bool { - // TLS encryption - if self.spec.cluster_config.tls.is_some() { - true - } else { - // TLS authentication with provided AuthenticationClass or no TLS required? - matches!( - &self.spec.cluster_config.authentication, - Some(DruidAuthentication { tls: Some(_) }) - ) - } - } } #[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] @@ -905,7 +847,7 @@ pub struct DruidClusterStatus {} /// Takes a vec of strings and returns them as a formatted json /// list. -fn build_string_list(strings: &[String]) -> String { +pub fn build_string_list(strings: &[String]) -> String { let quoted_strings: Vec = strings.iter().map(|s| format!("\"{}\"", s)).collect(); let comma_list = quoted_strings.join(", "); format!("[{}]", comma_list) @@ -932,7 +874,6 @@ pub fn build_recommended_labels<'a, T>( #[cfg(test)] mod tests { - use super::*; #[test] @@ -953,53 +894,4 @@ mod tests { Some("testcluster-router.default.svc.cluster.local".to_string()) ) } - - #[test] - fn test_druid_cluster_config_tls() { - let input = r#" - deepStorage: - hdfs: - configMapName: druid-hdfs - directory: /druid - metadataStorageDatabase: - dbType: derby - connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true - host: localhost - port: 1527 - zookeeperConfigMapName: zk-config-map - "#; - let druid_cluster_config: DruidClusterConfig = - serde_yaml::from_str(input).expect("illegal test input"); - - assert_eq!( - druid_cluster_config.zookeeper_config_map_name, - "zk-config-map".to_string() - ); - assert_eq!( - druid_cluster_config.tls.unwrap().secret_class, - DEFAULT_TLS_SECRET_CLASS.to_string() - ); - - let input = r#" - deepStorage: - hdfs: - configMapName: druid-hdfs - directory: /druid - metadataStorageDatabase: - dbType: derby - connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true - host: localhost - port: 1527 - tls: - secretClass: foo - zookeeperConfigMapName: zk-config-map - "#; - let druid_cluster_config: DruidClusterConfig = - serde_yaml::from_str(input).expect("illegal test input"); - - assert_eq!( - druid_cluster_config.tls.unwrap().secret_class, - "foo".to_string() - ); - } } diff --git a/rust/crd/src/security.rs b/rust/crd/src/security.rs new file mode 100644 index 00000000..b87b1e87 --- /dev/null +++ b/rust/crd/src/security.rs @@ -0,0 +1,445 @@ +use crate::{ + authentication::{self, ResolvedAuthenticationClasses}, + DruidCluster, DruidRole, METRICS_PORT, +}; +use snafu::{ResultExt, Snafu}; +use stackable_operator::{ + builder::{ContainerBuilder, PodBuilder, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + client::Client, + commons::authentication::AuthenticationClass, + k8s_openapi::{ + api::core::v1::{ContainerPort, Probe, ServicePort, TCPSocketAction}, + apimachinery::pkg::util::intstr::IntOrString, + }, +}; +use std::collections::BTreeMap; + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("failed to process authentication class"))] + InvalidAuthenticationClassConfiguration { source: authentication::Error }, +} + +/// Helper struct combining TLS settings for server and internal tls with the resolved AuthenticationClasses +pub struct DruidTlsSecurity { + resolved_authentication_classes: ResolvedAuthenticationClasses, + server_and_internal_secret_class: Option, +} + +impl DruidTlsSecurity { + // Ports + const ENABLE_PLAINTEXT_PORT: &str = "druid.enablePlaintextPort"; + const PLAINTEXT_PORT: &str = "druid.plaintextPort"; + const ENABLE_TLS_PORT: &str = "druid.enableTlsPort"; + const TLS_PORT: &str = "druid.tlsPort"; + // Port names + const PLAINTEXT_PORT_NAME: &str = "http"; + const TLS_PORT_NAME: &str = "https"; + const METRICS_PORT_NAME: &str = "metrics"; + // Client side (Druid) TLS + const CLIENT_HTTPS_KEY_STORE_PATH: &str = "druid.client.https.keyStorePath"; + const CLIENT_HTTPS_KEY_STORE_TYPE: &str = "druid.client.https.keyStoreType"; + const CLIENT_HTTPS_KEY_STORE_PASSWORD: &str = "druid.client.https.keyStorePassword"; + const CLIENT_HTTPS_TRUST_STORE_PATH: &str = "druid.client.https.trustStorePath"; + const CLIENT_HTTPS_TRUST_STORE_TYPE: &str = "druid.client.https.trustStoreType"; + const CLIENT_HTTPS_TRUST_STORE_PASSWORD: &str = "druid.client.https.trustStorePassword"; + const CLIENT_HTTPS_CERT_ALIAS: &str = "druid.client.https.certAlias"; + const CLIENT_HTTPS_VALIDATE_HOST_NAMES: &str = "druid.client.https.validateHostnames"; + const CLIENT_HTTPS_KEY_MANAGER_PASSWORD: &str = "druid.client.https.keyManagerPassword"; + // Server side TLS + const SERVER_HTTPS_KEY_STORE_PATH: &str = "druid.server.https.keyStorePath"; + const SERVER_HTTPS_KEY_STORE_TYPE: &str = "druid.server.https.keyStoreType"; + const SERVER_HTTPS_KEY_STORE_PASSWORD: &str = "druid.server.https.keyStorePassword"; + const SERVER_HTTPS_TRUST_STORE_PATH: &str = "druid.server.https.trustStorePath"; + const SERVER_HTTPS_TRUST_STORE_TYPE: &str = "druid.server.https.trustStoreType"; + const SERVER_HTTPS_TRUST_STORE_PASSWORD: &str = "druid.server.https.trustStorePassword"; + const SERVER_HTTPS_CERT_ALIAS: &str = "druid.server.https.certAlias"; + const SERVER_HTTPS_VALIDATE_HOST_NAMES: &str = "druid.server.https.validateHostnames"; + const SERVER_HTTPS_KEY_MANAGER_PASSWORD: &str = "druid.server.https.keyManagerPassword"; + const SERVER_HTTPS_REQUIRE_CLIENT_CERTIFICATE: &str = + "druid.server.https.requireClientCertificate"; + const TLS_ALIAS_NAME: &str = "tls"; + // Misc TLS + const TLS_STORE_PASSWORD: &str = "changeit"; + const TLS_STORE_TYPE: &str = "pkcs12"; + + // directories + const STACKABLE_MOUNT_TLS_DIR: &str = "/stackable/mount_tls"; + const STACKABLE_TLS_DIR: &str = "/stackable/tls"; + + pub fn new( + resolved_authentication_classes: ResolvedAuthenticationClasses, + server_and_internal_secret_class: Option, + ) -> Self { + Self { + resolved_authentication_classes, + server_and_internal_secret_class, + } + } + + /// Create a `DruidTlsSecurity` struct from the Druid custom resource and resolve + /// all provided `AuthenticationClass` references. + pub async fn new_from_druid_cluster( + client: &Client, + druid: &DruidCluster, + ) -> Result { + Ok(DruidTlsSecurity { + resolved_authentication_classes: + authentication::ResolvedAuthenticationClasses::from_references( + client, + druid, + &druid.spec.cluster_config.authentication, + ) + .await + .context(InvalidAuthenticationClassConfigurationSnafu)?, + server_and_internal_secret_class: druid + .spec + .cluster_config + .tls + .as_ref() + .and_then(|tls| tls.server_and_internal_secret_class.clone()), + }) + } + + /// Check if TLS encryption is enabled. This could be due to: + /// - A provided server `SecretClass` + /// - A provided client `AuthenticationClass` using tls + /// This affects init container commands, Druid configuration, volume mounts and + /// the Druid client port + pub fn tls_enabled(&self) -> bool { + // TODO: This must be adapted if other authentication methods are supported and require TLS + self.tls_client_authentication_class().is_some() + || self.tls_server_and_internal_secret_class().is_some() + } + + /// Retrieve an optional TLS secret class for external client -> server and server <-> server communications. + pub fn tls_server_and_internal_secret_class(&self) -> Option<&str> { + self.server_and_internal_secret_class.as_deref() + } + + /// Retrieve an optional TLS `AuthenticationClass`. + pub fn tls_client_authentication_class(&self) -> Option<&AuthenticationClass> { + self.resolved_authentication_classes + .get_tls_authentication_class() + } + + pub fn container_ports(&self, role: &DruidRole) -> Vec { + self.exposed_ports(role) + .into_iter() + .map(|(name, val)| ContainerPort { + name: Some(name), + container_port: val.into(), + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }) + .collect() + } + + pub fn service_ports(&self, role: &DruidRole) -> Vec { + self.exposed_ports(role) + .into_iter() + .map(|(name, val)| ServicePort { + name: Some(name), + port: val.into(), + protocol: Some("TCP".to_string()), + ..ServicePort::default() + }) + .collect() + } + + fn exposed_ports(&self, role: &DruidRole) -> Vec<(String, u16)> { + let mut ports = vec![(Self::METRICS_PORT_NAME.to_string(), METRICS_PORT)]; + + if self.tls_enabled() { + ports.push((Self::TLS_PORT_NAME.to_string(), role.get_https_port())); + } else { + ports.push((Self::PLAINTEXT_PORT_NAME.to_string(), role.get_http_port())); + } + + ports + } + + /// Adds required tls volume mounts to image and product container builders + /// Adds required tls volumes to pod builder + pub fn add_tls_volume_and_volume_mounts( + &self, + prepare: &mut ContainerBuilder, + druid: &mut ContainerBuilder, + pod: &mut PodBuilder, + ) -> Result<(), Error> { + // `ResolvedAuthenticationClasses::validate` already checked that the tls AuthenticationClass + // uses the same SecretClass as the Druid server itself. + if let Some(secret_class) = &self.server_and_internal_secret_class { + pod.add_volume( + VolumeBuilder::new("tls-mount") + .ephemeral( + SecretOperatorVolumeSourceBuilder::new(secret_class) + .with_pod_scope() + .with_node_scope() + .build(), + ) + .build(), + ); + prepare.add_volume_mount("tls-mount", Self::STACKABLE_MOUNT_TLS_DIR); + druid.add_volume_mount("tls-mount", Self::STACKABLE_MOUNT_TLS_DIR); + + pod.add_volume( + VolumeBuilder::new("tls") + .with_empty_dir(Option::<&str>::None, None) + .build(), + ); + prepare.add_volume_mount("tls", Self::STACKABLE_TLS_DIR); + druid.add_volume_mount("tls", Self::STACKABLE_TLS_DIR); + } + Ok(()) + } + + fn add_tls_port_config_properties( + &self, + config: &mut BTreeMap>, + role: &DruidRole, + ) { + // no secure communication + if !self.tls_enabled() { + config.insert( + Self::ENABLE_PLAINTEXT_PORT.to_string(), + Some("true".to_string()), + ); + config.insert(Self::ENABLE_TLS_PORT.to_string(), Some("false".to_string())); + config.insert( + Self::PLAINTEXT_PORT.to_string(), + Some(role.get_http_port().to_string()), + ); + } + // only allow secure communication + else { + config.insert( + Self::ENABLE_PLAINTEXT_PORT.to_string(), + Some("false".to_string()), + ); + config.insert(Self::ENABLE_TLS_PORT.to_string(), Some("true".to_string())); + config.insert( + Self::TLS_PORT.to_string(), + Some(role.get_https_port().to_string()), + ); + } + } + + /// Add required TLS ports, trust/key store properties + pub fn add_tls_config_properties( + &self, + config: &mut BTreeMap>, + role: &DruidRole, + ) { + self.add_tls_port_config_properties(config, role); + + if self.tls_enabled() { + Self::add_tls_encryption_config_properties( + config, + Self::STACKABLE_TLS_DIR, + Self::TLS_ALIAS_NAME, + ); + } + + if self + .resolved_authentication_classes + .get_tls_authentication_class() + .is_some() + { + Self::add_tls_auth_config_properties( + config, + Self::STACKABLE_TLS_DIR, + Self::TLS_ALIAS_NAME, + ); + } + } + + fn add_tls_encryption_config_properties( + config: &mut BTreeMap>, + store_directory: &str, + store_alias: &str, + ) { + // We need a truststore in addition to a keystore here, because server and internal tls + // can only be enabled/disabled together + config.insert( + Self::CLIENT_HTTPS_TRUST_STORE_PATH.to_string(), + Some(format!("{}/truststore.p12", store_directory)), + ); + config.insert( + Self::CLIENT_HTTPS_TRUST_STORE_TYPE.to_string(), + Some(Self::TLS_STORE_TYPE.to_string()), + ); + config.insert( + Self::CLIENT_HTTPS_TRUST_STORE_PASSWORD.to_string(), + Some(Self::TLS_STORE_PASSWORD.to_string()), + ); + + config.insert( + Self::SERVER_HTTPS_KEY_STORE_PATH.to_string(), + Some(format!("{}/keystore.p12", store_directory)), + ); + config.insert( + Self::SERVER_HTTPS_KEY_STORE_TYPE.to_string(), + Some(Self::TLS_STORE_TYPE.to_string()), + ); + config.insert( + Self::SERVER_HTTPS_KEY_STORE_PASSWORD.to_string(), + Some(Self::TLS_STORE_PASSWORD.to_string()), + ); + config.insert( + Self::SERVER_HTTPS_CERT_ALIAS.to_string(), + Some(store_alias.to_string()), + ); + } + + fn add_tls_auth_config_properties( + config: &mut BTreeMap>, + store_directory: &str, + store_alias: &str, + ) { + config.insert( + Self::CLIENT_HTTPS_KEY_STORE_PATH.to_string(), + Some(format!("{store_directory}/keystore.p12")), + ); + config.insert( + Self::CLIENT_HTTPS_KEY_STORE_TYPE.to_string(), + Some(Self::TLS_STORE_TYPE.to_string()), + ); + config.insert( + Self::CLIENT_HTTPS_KEY_STORE_PASSWORD.to_string(), + Some(Self::TLS_STORE_PASSWORD.to_string()), + ); + // This is required because PKCS12 does not use any key passwords but it will + // be checked and would lead to an exception: + // java.security.UnrecoverableKeyException: Get Key failed: null + // Must be set to the store password or we get a bad padding exception: + // javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. + config.insert( + Self::CLIENT_HTTPS_KEY_MANAGER_PASSWORD.to_string(), + Some(Self::TLS_STORE_PASSWORD.to_string()), + ); + config.insert( + Self::CLIENT_HTTPS_CERT_ALIAS.to_string(), + Some(store_alias.to_string()), + ); + // FIXME: https://github.com/stackabletech/druid-operator/issues/372 + // This is required because the server will send its pod ip which is not in the SANs of the certificates + config.insert( + Self::CLIENT_HTTPS_VALIDATE_HOST_NAMES.to_string(), + Some("false".to_string()), + ); + + // This will enforce the client to authenticate itself + config.insert( + Self::SERVER_HTTPS_REQUIRE_CLIENT_CERTIFICATE.to_string(), + Some("true".to_string()), + ); + + config.insert( + Self::SERVER_HTTPS_TRUST_STORE_PATH.to_string(), + Some(format!("{store_directory}/truststore.p12")), + ); + config.insert( + Self::SERVER_HTTPS_TRUST_STORE_TYPE.to_string(), + Some(Self::TLS_STORE_TYPE.to_string()), + ); + config.insert( + Self::SERVER_HTTPS_TRUST_STORE_PASSWORD.to_string(), + Some(Self::TLS_STORE_PASSWORD.to_string()), + ); + // This is required because PKCS12 does not use any key passwords but it will + // be checked and would lead to an exception: + // java.security.UnrecoverableKeyException: Get Key failed: null + // Must be set to the store password or we get a bad padding exception: + // javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. + config.insert( + Self::SERVER_HTTPS_KEY_MANAGER_PASSWORD.to_string(), + Some(Self::TLS_STORE_PASSWORD.to_string()), + ); + // FIXME: https://github.com/stackabletech/druid-operator/issues/372 + // This is required because the client will send its pod ip which is not in the SANs of the certificates + config.insert( + Self::SERVER_HTTPS_VALIDATE_HOST_NAMES.to_string(), + Some("false".to_string()), + ); + } + + pub fn build_tls_key_stores_cmd(&self) -> Vec { + let mut command = vec![]; + if self.tls_enabled() { + command.extend(add_cert_to_trust_store_cmd( + Self::STACKABLE_MOUNT_TLS_DIR, + Self::STACKABLE_TLS_DIR, + Self::TLS_ALIAS_NAME, + Self::TLS_STORE_PASSWORD, + )); + command.extend(add_key_pair_to_key_store_cmd( + Self::STACKABLE_MOUNT_TLS_DIR, + Self::STACKABLE_TLS_DIR, + Self::TLS_ALIAS_NAME, + Self::TLS_STORE_PASSWORD, + )); + } + command + } + + pub fn get_tcp_socket_probe( + &self, + initial_delay_seconds: i32, + period_seconds: i32, + failure_threshold: i32, + timeout_seconds: i32, + ) -> Probe { + let port = if self.tls_enabled() { + IntOrString::String(Self::TLS_PORT_NAME.to_string()) + } else { + IntOrString::String(Self::PLAINTEXT_PORT_NAME.to_string()) + }; + + Probe { + tcp_socket: Some(TCPSocketAction { + port, + ..Default::default() + }), + initial_delay_seconds: Some(initial_delay_seconds), + period_seconds: Some(period_seconds), + failure_threshold: Some(failure_threshold), + timeout_seconds: Some(timeout_seconds), + ..Default::default() + } + } +} + +/// Generate a script to add a CA to a truststore +pub fn add_cert_to_trust_store_cmd( + cert_directory: &str, + trust_store_directory: &str, + alias_name: &str, + store_password: &str, +) -> Vec { + vec![ + format!( + "echo Cleaning up truststore [{trust_store_directory}/truststore.p12] - just in case" + ), + format!("rm -f {trust_store_directory}/truststore.p12"), + format!( + "echo Creating truststore [{trust_store_directory}/truststore.p12]" + ), + format!("keytool -importcert -file {cert_directory}/ca.crt -keystore {trust_store_directory}/truststore.p12 -storetype pkcs12 -alias {alias_name} -storepass {store_password} -noprompt") + ] +} + +/// Generate a script to create a certificate chain and add a key-cert pair to the keystore +pub fn add_key_pair_to_key_store_cmd( + cert_directory: &str, + key_store_directory: &str, + alias_name: &str, + store_password: &str, +) -> Vec { + vec![ + format!("echo Creating certificate chain [{key_store_directory}/chain.crt]"), + format!("cat {cert_directory}/ca.crt {cert_directory}/tls.crt > {key_store_directory}/chain.crt"), + format!("echo Creating keystore [{key_store_directory}/keystore.p12]"), + format!("openssl pkcs12 -export -in {key_store_directory}/chain.crt -inkey {cert_directory}/tls.key -out {key_store_directory}/keystore.p12 --passout pass:{store_password} -name {alias_name}"), + ] +} diff --git a/rust/crd/src/tls.rs b/rust/crd/src/tls.rs index f5779e95..6a0419b3 100644 --- a/rust/crd/src/tls.rs +++ b/rust/crd/src/tls.rs @@ -1,579 +1,144 @@ -use crate::authentication::DruidAuthenticationConfig; -use crate::{DruidRole, METRICS_PORT}; - use serde::{Deserialize, Serialize}; -use snafu::Snafu; -use stackable_operator::{ - builder::{ContainerBuilder, PodBuilder, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, - k8s_openapi::{ - api::core::v1::{ContainerPort, Probe, ServicePort, TCPSocketAction, Volume}, - apimachinery::pkg::util::intstr::IntOrString, - }, - schemars::{self, JsonSchema}, -}; -use std::collections::BTreeMap; -use strum::{EnumDiscriminants, IntoStaticStr}; - -// Ports -pub const ENABLE_PLAINTEXT_PORT: &str = "druid.enablePlaintextPort"; -pub const PLAINTEXT_PORT: &str = "druid.plaintextPort"; -pub const ENABLE_TLS_PORT: &str = "druid.enableTlsPort"; -pub const TLS_PORT: &str = "druid.tlsPort"; -// Port names -const PLAINTEXT_PORT_NAME: &str = "http"; -const TLS_PORT_NAME: &str = "https"; -const METRICS_PORT_NAME: &str = "metrics"; -// Client side (Druid) TLS -pub const CLIENT_HTTPS_KEY_STORE_PATH: &str = "druid.client.https.keyStorePath"; -pub const CLIENT_HTTPS_KEY_STORE_TYPE: &str = "druid.client.https.keyStoreType"; -pub const CLIENT_HTTPS_KEY_STORE_PASSWORD: &str = "druid.client.https.keyStorePassword"; -pub const CLIENT_HTTPS_TRUST_STORE_PATH: &str = "druid.client.https.trustStorePath"; -pub const CLIENT_HTTPS_TRUST_STORE_TYPE: &str = "druid.client.https.trustStoreType"; -pub const CLIENT_HTTPS_TRUST_STORE_PASSWORD: &str = "druid.client.https.trustStorePassword"; -pub const CLIENT_HTTPS_CERT_ALIAS: &str = "druid.client.https.certAlias"; -pub const CLIENT_HTTPS_VALIDATE_HOST_NAMES: &str = "druid.client.https.validateHostnames"; -pub const CLIENT_HTTPS_KEY_MANAGER_PASSWORD: &str = "druid.client.https.keyManagerPassword"; -// Server side TLS -pub const SERVER_HTTPS_KEY_STORE_PATH: &str = "druid.server.https.keyStorePath"; -pub const SERVER_HTTPS_KEY_STORE_TYPE: &str = "druid.server.https.keyStoreType"; -pub const SERVER_HTTPS_KEY_STORE_PASSWORD: &str = "druid.server.https.keyStorePassword"; -pub const SERVER_HTTPS_TRUST_STORE_PATH: &str = "druid.server.https.trustStorePath"; -pub const SERVER_HTTPS_TRUST_STORE_TYPE: &str = "druid.server.https.trustStoreType"; -pub const SERVER_HTTPS_TRUST_STORE_PASSWORD: &str = "druid.server.https.trustStorePassword"; -pub const SERVER_HTTPS_CERT_ALIAS: &str = "druid.server.https.certAlias"; -pub const SERVER_HTTPS_VALIDATE_HOST_NAMES: &str = "druid.server.https.validateHostnames"; -pub const SERVER_HTTPS_KEY_MANAGER_PASSWORD: &str = "druid.server.https.keyManagerPassword"; -pub const SERVER_HTTPS_REQUIRE_CLIENT_CERTIFICATE: &str = - "druid.server.https.requireClientCertificate"; -pub const TLS_ALIAS_NAME: &str = "tls"; -// Misc TLS -pub const TLS_STORE_PASSWORD: &str = "changeit"; -pub const TLS_STORE_TYPE: &str = "pkcs12"; - -// directories -pub const STACKABLE_MOUNT_TLS_DIR: &str = "/stackable/mount_tls"; -pub const STACKABLE_TLS_DIR: &str = "/stackable/tls"; +use stackable_operator::schemars::{self, JsonSchema}; -#[derive(Snafu, Debug, EnumDiscriminants)] -#[strum_discriminants(derive(IntoStaticStr))] -pub enum Error { - #[snafu(display("The provided TLS configuration is invalid: {reason}"))] - InvalidTlsConfiguration { reason: String }, -} +const TLS_DEFAULT_SECRET_CLASS: &str = "tls"; -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, JsonSchema, Serialize)] #[serde(rename_all = "camelCase")] pub struct DruidTls { - /// Only affects client connections. - /// This setting controls: + /// This setting controls client as well as internal tls usage: /// - If TLS encryption is used at all - /// - Which cert the servers should use to authenticate themselves against the client - /// Important: This will activate encrypted internal druid communication as well! + /// - Which cert the servers should use to authenticate themselves against the clients + /// - Which cert the servers should use to authenticate themselves among each other // TODO: Separating internal and server TLS is currently not possible. Internal communication - // happens via the HTTPS port. Even if both HTTPS and HTTP port are enabled, Druid clients - // will default to using TLS. - pub secret_class: String, -} - -/// This is a struct to bundle TLS encryption and TLS authentication. Helper methods contain: -/// - Which config properties must be set -/// - Which extension should be loaded -/// - Which volume and volume mounts to be set -/// - Which container and service ports to be set -/// - Which init container commands to be set -/// - Which probes to be set -pub struct DruidTlsSettings { - pub encryption: Option, - pub authentication: Option, -} - -impl DruidTlsSettings { - pub fn container_ports(&self, role: &DruidRole) -> Vec { - self.exposed_ports(role) - .into_iter() - .map(|(name, val)| ContainerPort { - name: Some(name), - container_port: val.into(), - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }) - .collect() - } - - pub fn service_ports(&self, role: &DruidRole) -> Vec { - self.exposed_ports(role) - .into_iter() - .map(|(name, val)| ServicePort { - name: Some(name), - port: val.into(), - protocol: Some("TCP".to_string()), - ..ServicePort::default() - }) - .collect() - } - - fn exposed_ports(&self, role: &DruidRole) -> Vec<(String, u16)> { - let mut ports = vec![(METRICS_PORT_NAME.to_string(), METRICS_PORT)]; - - if self.encryption.is_none() && self.authentication.is_none() { - ports.push((PLAINTEXT_PORT_NAME.to_string(), role.get_http_port())); - } else { - ports.push((TLS_PORT_NAME.to_string(), role.get_https_port())); - } - - ports - } - - /// Adds required tls volume mounts to image and product container builders - /// Adds required tls volumes to pod builder - pub fn add_tls_volume_and_volume_mounts( - &self, - prepare: &mut ContainerBuilder, - druid: &mut ContainerBuilder, - pod: &mut PodBuilder, - ) -> Result<(), Error> { - let secret_class = if let Some(DruidAuthenticationConfig::Tls(provider)) = - &self.authentication - { - // If client_cert_secret_class authentication is set use it for our tls volume mounts - if provider.client_cert_secret_class.is_some() { - provider.client_cert_secret_class.as_ref() - } - // If the client_cert_secret_class is not set, we require to have a TLS encryption secret class set - else if let Some(tls) = &self.encryption { - Some(&tls.secret_class) - } - // This is a bad configuration (TLS auth required, but certificates are neither provided in the AuthenticationClass nor in the TLS encryption SecretClass - else { - return Err(Error::InvalidTlsConfiguration { - reason: "TLS client authentication is required but no certificates are provided in the \ - `spec.cluster_config.authentication.tls.authenticationClass` \ - or in the `spec.cluster_config.tls.secretClass` encryption settings".to_string() - }); - } - } else { - self.encryption.as_ref().map(|tls| &tls.secret_class) - }; - - if let Some(secret_class) = secret_class { - prepare.add_volume_mount("tls-mount", STACKABLE_MOUNT_TLS_DIR); - druid.add_volume_mount("tls-mount", STACKABLE_MOUNT_TLS_DIR); - pod.add_volume(create_tls_volume("tls-mount", secret_class)); - - prepare.add_volume_mount("tls", STACKABLE_TLS_DIR); - druid.add_volume_mount("tls", STACKABLE_TLS_DIR); - pod.add_volume( - VolumeBuilder::new("tls") - .with_empty_dir(Some(""), None) - .build(), - ); - } - - Ok(()) - } - - fn add_tls_port_config_properties( - &self, - config: &mut BTreeMap>, - role: &DruidRole, - ) { - // no secure communication - if self.encryption.is_none() && self.authentication.is_none() { - config.insert(ENABLE_PLAINTEXT_PORT.to_string(), Some("true".to_string())); - config.insert(ENABLE_TLS_PORT.to_string(), Some("false".to_string())); - config.insert( - PLAINTEXT_PORT.to_string(), - Some(role.get_http_port().to_string()), - ); - } - // only secure communication - else { - config.insert(ENABLE_PLAINTEXT_PORT.to_string(), Some("false".to_string())); - config.insert(ENABLE_TLS_PORT.to_string(), Some("true".to_string())); - config.insert( - TLS_PORT.to_string(), - Some(role.get_https_port().to_string()), - ); - } - } - - /// Add required TLS ports, trust/key store properties - pub fn add_tls_config_properties( - &self, - config: &mut BTreeMap>, - role: &DruidRole, - ) { - self.add_tls_port_config_properties(config, role); - - if self.encryption.is_some() || self.authentication.is_some() { - Self::add_tls_encryption_config_properties(config, STACKABLE_TLS_DIR, TLS_ALIAS_NAME); - } - - if self.authentication.is_some() { - Self::add_tls_auth_config_properties(config, STACKABLE_TLS_DIR, TLS_ALIAS_NAME); - } - } - - fn add_tls_encryption_config_properties( - config: &mut BTreeMap>, - store_directory: &str, - store_alias: &str, - ) { - config.insert( - CLIENT_HTTPS_TRUST_STORE_PATH.to_string(), - Some(format!("{}/truststore.p12", store_directory)), - ); - config.insert( - CLIENT_HTTPS_TRUST_STORE_TYPE.to_string(), - Some(TLS_STORE_TYPE.to_string()), - ); - config.insert( - CLIENT_HTTPS_TRUST_STORE_PASSWORD.to_string(), - Some(TLS_STORE_PASSWORD.to_string()), - ); - - config.insert( - SERVER_HTTPS_KEY_STORE_PATH.to_string(), - Some(format!("{}/keystore.p12", store_directory)), - ); - config.insert( - SERVER_HTTPS_KEY_STORE_TYPE.to_string(), - Some(TLS_STORE_TYPE.to_string()), - ); - config.insert( - SERVER_HTTPS_KEY_STORE_PASSWORD.to_string(), - Some(TLS_STORE_PASSWORD.to_string()), - ); - config.insert( - SERVER_HTTPS_CERT_ALIAS.to_string(), - Some(store_alias.to_string()), - ); - } - - fn add_tls_auth_config_properties( - config: &mut BTreeMap>, - store_directory: &str, - store_alias: &str, - ) { - config.insert( - CLIENT_HTTPS_KEY_STORE_PATH.to_string(), - Some(format!("{}/keystore.p12", store_directory)), - ); - config.insert( - CLIENT_HTTPS_KEY_STORE_TYPE.to_string(), - Some(TLS_STORE_TYPE.to_string()), - ); - config.insert( - CLIENT_HTTPS_KEY_STORE_PASSWORD.to_string(), - Some(TLS_STORE_PASSWORD.to_string()), - ); - // This is required because PKCS12 does not use any key passwords but it will - // be checked and would lead to an exception: - // java.security.UnrecoverableKeyException: Get Key failed: null - // Must be set to the store password or we get a bad padding exception: - // javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. - config.insert( - CLIENT_HTTPS_KEY_MANAGER_PASSWORD.to_string(), - Some(TLS_STORE_PASSWORD.to_string()), - ); - config.insert( - CLIENT_HTTPS_CERT_ALIAS.to_string(), - Some(store_alias.to_string()), - ); - // This is required because the server will send its pod ip which is not in the SANs of the certificates - config.insert( - CLIENT_HTTPS_VALIDATE_HOST_NAMES.to_string(), - Some("false".to_string()), - ); - - // This will enforce the client to authenticate itself - config.insert( - SERVER_HTTPS_REQUIRE_CLIENT_CERTIFICATE.to_string(), - Some("true".to_string()), - ); - - config.insert( - SERVER_HTTPS_TRUST_STORE_PATH.to_string(), - Some(format!("{}/truststore.p12", store_directory)), - ); - config.insert( - SERVER_HTTPS_TRUST_STORE_TYPE.to_string(), - Some(TLS_STORE_TYPE.to_string()), - ); - config.insert( - SERVER_HTTPS_TRUST_STORE_PASSWORD.to_string(), - Some(TLS_STORE_PASSWORD.to_string()), - ); - // This is required because PKCS12 does not use any key passwords but it will - // be checked and would lead to an exception: - // java.security.UnrecoverableKeyException: Get Key failed: null - // Must be set to the store password or we get a bad padding exception: - // javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. - config.insert( - SERVER_HTTPS_KEY_MANAGER_PASSWORD.to_string(), - Some(TLS_STORE_PASSWORD.to_string()), - ); - // This is required because the client will send its pod ip which is not in the SANs of the certificates - config.insert( - SERVER_HTTPS_VALIDATE_HOST_NAMES.to_string(), - Some("false".to_string()), - ); - } - - pub fn build_tls_key_stores_cmd(&self) -> Vec { - let mut command = vec![]; - if self.encryption.is_some() || self.authentication.is_some() { - command.extend(add_cert_to_trust_store_cmd( - STACKABLE_MOUNT_TLS_DIR, - STACKABLE_TLS_DIR, - TLS_ALIAS_NAME, - TLS_STORE_PASSWORD, - )); - command.extend(add_key_pair_to_key_store_cmd( - STACKABLE_MOUNT_TLS_DIR, - STACKABLE_TLS_DIR, - TLS_ALIAS_NAME, - TLS_STORE_PASSWORD, - )); - } - command - } - - pub fn get_tcp_socket_probe( - &self, - initial_delay_seconds: i32, - period_seconds: i32, - failure_threshold: i32, - timeout_seconds: i32, - ) -> Probe { - let port = if self.encryption.is_some() || self.authentication.is_some() { - IntOrString::String(TLS_PORT_NAME.to_string()) - } else { - IntOrString::String(PLAINTEXT_PORT_NAME.to_string()) - }; - - Probe { - tcp_socket: Some(TCPSocketAction { - port, - ..Default::default() - }), - initial_delay_seconds: Some(initial_delay_seconds), - period_seconds: Some(period_seconds), - failure_threshold: Some(failure_threshold), - timeout_seconds: Some(timeout_seconds), - ..Default::default() - } - } + // happens via the HTTPS port. Even if both HTTPS and HTTP port are enabled, Druid clients + // will default to using TLS. + #[serde(default = "tls_default", skip_serializing_if = "Option::is_none")] + pub server_and_internal_secret_class: Option, } -/// Generate a script to add a CA to a truststore -pub fn add_cert_to_trust_store_cmd( - cert_directory: &str, - trust_store_directory: &str, - alias_name: &str, - store_password: &str, -) -> Vec { - let mut command = vec![]; - command.push(format!( - "echo Cleaning up truststore [{trust_store_directory}/truststore.p12] - just in case" - )); - command.push(format!("rm -f {trust_store_directory}/truststore.p12")); - command.push(format!( - "echo Creating truststore [{trust_store_directory}/truststore.p12]" - )); - command.push(format!("keytool -importcert -file {cert_directory}/ca.crt -keystore {trust_store_directory}/truststore.p12 -storetype pkcs12 -alias {alias_name} -storepass {store_password} -noprompt")); - - command -} - -/// Generate a script to create a certificate chain and add a key-cert pair to the keystore -pub fn add_key_pair_to_key_store_cmd( - cert_directory: &str, - key_store_directory: &str, - alias_name: &str, - store_password: &str, -) -> Vec { - vec![ - format!("echo Creating certificate chain [{key_store_directory}/chain.crt]"), - format!("cat {cert_directory}/ca.crt {cert_directory}/tls.crt > {key_store_directory}/chain.crt"), - format!("echo Creating keystore [{key_store_directory}/keystore.p12]"), - format!("openssl pkcs12 -export -in {key_store_directory}/chain.crt -inkey {cert_directory}/tls.key -out {key_store_directory}/keystore.p12 --passout pass:{store_password} -name {alias_name}"), - ] +/// Default TLS settings. Internal and server communication default to "tls" secret class. +pub fn default_druid_tls() -> Option { + Some(DruidTls { + server_and_internal_secret_class: tls_default(), + }) } -/// Create an ephemeral TLS volume -pub fn create_tls_volume(volume_name: &str, tls_secret_class: &str) -> Volume { - VolumeBuilder::new(volume_name) - .ephemeral( - SecretOperatorVolumeSourceBuilder::new(tls_secret_class) - .with_pod_scope() - .with_node_scope() - .build(), - ) - .build() +/// Helper methods to provide defaults in the CRDs and tests +pub fn tls_default() -> Option { + Some(TLS_DEFAULT_SECRET_CLASS.to_string()) } #[cfg(test)] mod tests { - use super::*; - use crate::DEFAULT_TLS_SECRET_CLASS; - use stackable_operator::commons::tls::TlsAuthenticationProvider; + use crate::{authentication::DruidAuthentication, tls::DruidTls, DruidClusterConfig}; + use indoc::formatdoc; + + const BASE_DRUID_CONFIGURATION: &str = r#" +deepStorage: + hdfs: + configMapName: druid-hdfs + directory: /druid +metadataStorageDatabase: + dbType: derby + connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true + host: localhost + port: 1527 +zookeeperConfigMapName: zk-config-map + "#; #[test] - fn test_add_tls_config_properties_no_encryption_no_authentication() { - let tls_settings_encryption = DruidTlsSettings { - encryption: None, - authentication: None, - }; - - let mut config = BTreeMap::new(); - let role = DruidRole::Router; - let port = role.get_http_port().to_string(); - tls_settings_encryption.add_tls_config_properties(&mut config, &role); - - let check = vec![ - (ENABLE_PLAINTEXT_PORT, "true"), - (ENABLE_TLS_PORT, "false"), - (PLAINTEXT_PORT, &port), - ]; - - for (key, value) in check { - assert_eq!(config.get(key).unwrap().as_deref(), Some(value)); - } + fn test_tls_default() { + let druid_cluster_config: DruidClusterConfig = + serde_yaml::from_str(BASE_DRUID_CONFIGURATION).expect("illegal test input"); + + assert_eq!( + druid_cluster_config.tls, + Some(DruidTls { + server_and_internal_secret_class: Some("tls".to_string()) + }), + ); + assert_eq!(druid_cluster_config.authentication, vec![]); } #[test] - fn test_add_tls_config_properties_only_encryption() { - let tls_settings_encryption = DruidTlsSettings { - encryption: Some(DruidTls { - secret_class: DEFAULT_TLS_SECRET_CLASS.to_string(), + fn test_tls_explicit_enabled() { + let input = formatdoc! {"\ + {BASE_DRUID_CONFIGURATION} + tls: + serverAndInternalSecretClass: druid-secret-class + "}; + dbg!(&input); + let druid_cluster_config: DruidClusterConfig = + serde_yaml::from_str(&input).expect("illegal test input"); + + assert_eq!( + druid_cluster_config.tls, + Some(DruidTls { + server_and_internal_secret_class: Some("druid-secret-class".to_string()) }), - authentication: None, - }; - - let mut config = BTreeMap::new(); - let role = DruidRole::Router; - let port = role.get_https_port().to_string(); - let truststore_path = format!("{}/truststore.p12", STACKABLE_TLS_DIR); - let keystore_path = format!("{}/keystore.p12", STACKABLE_TLS_DIR); - - tls_settings_encryption.add_tls_config_properties(&mut config, &role); - - let check = vec![ - (ENABLE_PLAINTEXT_PORT, "false"), - (ENABLE_TLS_PORT, "true"), - (TLS_PORT, &port), - (CLIENT_HTTPS_TRUST_STORE_PATH, &truststore_path), - (CLIENT_HTTPS_TRUST_STORE_TYPE, TLS_STORE_TYPE), - (CLIENT_HTTPS_TRUST_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_KEY_STORE_PATH, &keystore_path), - (SERVER_HTTPS_KEY_STORE_TYPE, TLS_STORE_TYPE), - (SERVER_HTTPS_KEY_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - ]; - - for (key, value) in check { - assert_eq!(config.get(key).unwrap().as_deref(), Some(value)); - } + ); + assert_eq!(druid_cluster_config.authentication, vec![]); } #[test] - fn test_add_tls_config_properties_only_authentication() { - let tls_settings_encryption = DruidTlsSettings { - encryption: None, - authentication: Some(DruidAuthenticationConfig::Tls(TlsAuthenticationProvider { - client_cert_secret_class: Some(DEFAULT_TLS_SECRET_CLASS.to_string()), - })), - }; - - let mut config = BTreeMap::new(); - let role = DruidRole::Router; - let port = role.get_https_port().to_string(); - let truststore_path = format!("{}/truststore.p12", STACKABLE_TLS_DIR); - let keystore_path = format!("{}/keystore.p12", STACKABLE_TLS_DIR); - - tls_settings_encryption.add_tls_config_properties(&mut config, &role); - - let check = vec![ - (ENABLE_PLAINTEXT_PORT, "false"), - (ENABLE_TLS_PORT, "true"), - (TLS_PORT, &port), - // tls - (CLIENT_HTTPS_TRUST_STORE_PATH, &truststore_path), - (CLIENT_HTTPS_TRUST_STORE_TYPE, TLS_STORE_TYPE), - (CLIENT_HTTPS_TRUST_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_KEY_STORE_PATH, &keystore_path), - (SERVER_HTTPS_KEY_STORE_TYPE, TLS_STORE_TYPE), - (SERVER_HTTPS_KEY_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - // auth - (CLIENT_HTTPS_KEY_STORE_PATH, &keystore_path), - (CLIENT_HTTPS_KEY_STORE_TYPE, TLS_STORE_TYPE), - (CLIENT_HTTPS_KEY_STORE_PASSWORD, TLS_STORE_PASSWORD), - (CLIENT_HTTPS_KEY_MANAGER_PASSWORD, TLS_STORE_PASSWORD), - (CLIENT_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - (CLIENT_HTTPS_VALIDATE_HOST_NAMES, "false"), - (SERVER_HTTPS_TRUST_STORE_PATH, &truststore_path), - (SERVER_HTTPS_TRUST_STORE_TYPE, TLS_STORE_TYPE), - (SERVER_HTTPS_TRUST_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - (SERVER_HTTPS_REQUIRE_CLIENT_CERTIFICATE, "true"), - (SERVER_HTTPS_VALIDATE_HOST_NAMES, "false"), - ]; - - for (key, value) in check { - assert_eq!(config.get(key).unwrap().as_deref(), Some(value)); - } + fn test_tls_explicit_disabled() { + let input = formatdoc! {"\ + {BASE_DRUID_CONFIGURATION} + tls: null + "}; + dbg!(&input); + let druid_cluster_config: DruidClusterConfig = + serde_yaml::from_str(&input).expect("illegal test input"); + + assert_eq!(druid_cluster_config.tls, None,); + assert_eq!(druid_cluster_config.authentication, vec![]); } #[test] - fn test_add_tls_config_properties_encryption_and_authentication() { - let tls_settings_encryption = DruidTlsSettings { - encryption: Some(DruidTls { - secret_class: DEFAULT_TLS_SECRET_CLASS.to_string(), + fn test_tls_explicit_disabled_secret_class() { + let input = formatdoc! {"\ + {BASE_DRUID_CONFIGURATION} + tls: + serverAndInternalSecretClass: null + "}; + dbg!(&input); + let druid_cluster_config: DruidClusterConfig = + serde_yaml::from_str(&input).expect("illegal test input"); + + assert_eq!( + druid_cluster_config.tls, + Some(DruidTls { + server_and_internal_secret_class: None, }), - authentication: Some(DruidAuthenticationConfig::Tls(TlsAuthenticationProvider { - client_cert_secret_class: Some(DEFAULT_TLS_SECRET_CLASS.to_string()), - })), - }; - - let mut config = BTreeMap::new(); - let role = DruidRole::Router; - let port = role.get_https_port().to_string(); - let truststore_path = format!("{}/truststore.p12", STACKABLE_TLS_DIR); - let keystore_path = format!("{}/keystore.p12", STACKABLE_TLS_DIR); - - tls_settings_encryption.add_tls_config_properties(&mut config, &role); - - let check = vec![ - (ENABLE_PLAINTEXT_PORT, "false"), - (ENABLE_TLS_PORT, "true"), - (TLS_PORT, &port), - // tls - (CLIENT_HTTPS_TRUST_STORE_PATH, &truststore_path), - (CLIENT_HTTPS_TRUST_STORE_TYPE, TLS_STORE_TYPE), - (CLIENT_HTTPS_TRUST_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_KEY_STORE_PATH, &keystore_path), - (SERVER_HTTPS_KEY_STORE_TYPE, TLS_STORE_TYPE), - (SERVER_HTTPS_KEY_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - // auth - (CLIENT_HTTPS_KEY_STORE_PATH, &keystore_path), - (CLIENT_HTTPS_KEY_STORE_TYPE, TLS_STORE_TYPE), - (CLIENT_HTTPS_KEY_STORE_PASSWORD, TLS_STORE_PASSWORD), - (CLIENT_HTTPS_KEY_MANAGER_PASSWORD, TLS_STORE_PASSWORD), - (CLIENT_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - (CLIENT_HTTPS_VALIDATE_HOST_NAMES, "false"), - (SERVER_HTTPS_TRUST_STORE_PATH, &truststore_path), - (SERVER_HTTPS_TRUST_STORE_TYPE, TLS_STORE_TYPE), - (SERVER_HTTPS_TRUST_STORE_PASSWORD, TLS_STORE_PASSWORD), - (SERVER_HTTPS_CERT_ALIAS, TLS_ALIAS_NAME), - (SERVER_HTTPS_REQUIRE_CLIENT_CERTIFICATE, "true"), - (SERVER_HTTPS_VALIDATE_HOST_NAMES, "false"), - ]; + ); + assert_eq!(druid_cluster_config.authentication, vec![]); + } - for (key, value) in check { - assert_eq!(config.get(key).unwrap().as_deref(), Some(value)); - } + #[test] + fn test_tls_explicit_enabled_and_authentication_enabled() { + let input = formatdoc! {"\ + {BASE_DRUID_CONFIGURATION} + tls: + serverAndInternalSecretClass: druid-secret-class + authentication: + - authenticationClass: druid-user-authentication-class + "}; + dbg!(&input); + let druid_cluster_config: DruidClusterConfig = + serde_yaml::from_str(&input).expect("illegal test input"); + + assert_eq!( + druid_cluster_config.tls, + Some(DruidTls { + server_and_internal_secret_class: Some("druid-secret-class".to_string()) + }), + ); + assert_eq!( + druid_cluster_config.authentication, + vec![DruidAuthentication { + authentication_class: "druid-user-authentication-class".to_string() + }], + ); } } diff --git a/rust/crd/test/resources/resource_merge/druid_cluster.yaml b/rust/crd/test/resources/resource_merge/druid_cluster.yaml index dd5c5549..401fb5e0 100644 --- a/rust/crd/test/resources/resource_merge/druid_cluster.yaml +++ b/rust/crd/test/resources/resource_merge/druid_cluster.yaml @@ -21,7 +21,6 @@ spec: port: 5432 user: druid password: druid - tls: null zookeeperConfigMapName: psql-druid-znode brokers: roleGroups: diff --git a/rust/crd/test/resources/resource_merge/segment_cache.yaml b/rust/crd/test/resources/resource_merge/segment_cache.yaml index a9918ff2..d85ece29 100644 --- a/rust/crd/test/resources/resource_merge/segment_cache.yaml +++ b/rust/crd/test/resources/resource_merge/segment_cache.yaml @@ -21,7 +21,6 @@ spec: port: 5432 user: druid password: druid - tls: null zookeeperConfigMapName: psql-druid-znode brokers: roleGroups: diff --git a/rust/crd/test/resources/role_service/druid_cluster.yaml b/rust/crd/test/resources/role_service/druid_cluster.yaml index d26c201b..c6f40787 100644 --- a/rust/crd/test/resources/role_service/druid_cluster.yaml +++ b/rust/crd/test/resources/role_service/druid_cluster.yaml @@ -21,7 +21,6 @@ spec: port: 5432 user: druid password: druid - tls: null zookeeperConfigMapName: psql-druid-znode brokers: roleGroups: diff --git a/rust/operator-binary/src/discovery.rs b/rust/operator-binary/src/discovery.rs index 6466b815..cf3d15be 100644 --- a/rust/operator-binary/src/discovery.rs +++ b/rust/operator-binary/src/discovery.rs @@ -4,7 +4,9 @@ use crate::CONTROLLER_NAME; use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_druid_crd::{build_recommended_labels, DruidCluster, DruidRole}; +use stackable_druid_crd::{ + build_recommended_labels, security::DruidTlsSecurity, DruidCluster, DruidRole, +}; use stackable_operator::{ builder::{ConfigMapBuilder, ObjectMetaBuilder}, commons::product_image_selection::ResolvedProductImage, @@ -32,12 +34,14 @@ pub async fn build_discovery_configmaps( druid: &DruidCluster, owner: &impl Resource, resolved_product_image: &ResolvedProductImage, + druid_tls_security: &DruidTlsSecurity, ) -> Result, Error> { let name = owner.name_unchecked(); Ok(vec![build_discovery_configmap( druid, owner, resolved_product_image, + druid_tls_security, &name, )?]) } @@ -47,6 +51,7 @@ fn build_discovery_configmap( druid: &DruidCluster, owner: &impl Resource, resolved_product_image: &ResolvedProductImage, + druid_tls_security: &DruidTlsSecurity, name: &str, ) -> Result { let router_host = format!( @@ -54,7 +59,7 @@ fn build_discovery_configmap( druid .role_service_fqdn(&DruidRole::Router) .with_context(|| NoServiceFqdnSnafu)?, - if druid.tls_enabled() { + if druid_tls_security.tls_enabled() { DruidRole::Router.get_https_port() } else { DruidRole::Router.get_http_port() diff --git a/rust/operator-binary/src/druid_controller.rs b/rust/operator-binary/src/druid_controller.rs index 8f994ca5..b8b9c90e 100644 --- a/rust/operator-binary/src/druid_controller.rs +++ b/rust/operator-binary/src/druid_controller.rs @@ -2,21 +2,21 @@ use crate::{ config::{get_jvm_config, get_log4j_config}, discovery::{self, build_discovery_configmaps}, + extensions::get_extension_list, }; use crate::OPERATOR_NAME; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_druid_crd::{ - authentication, authentication::DruidAuthentication, tls, DeepStorageSpec, DruidAuthorization, - DruidCluster, DruidRole, APP_NAME, AUTH_AUTHORIZER_OPA_URI, CERTS_DIR, - CREDENTIALS_SECRET_PROPERTY, DRUID_CONFIG_DIRECTORY, DS_BUCKET, HDFS_CONFIG_DIRECTORY, - JVM_CONFIG, LOG4J2_CONFIG, RUNTIME_PROPS, RW_CONFIG_DIRECTORY, S3_ENDPOINT_URL, - S3_PATH_STYLE_ACCESS, S3_SECRET_DIR_NAME, ZOOKEEPER_CONNECTION_STRING, + authorization::DruidAuthorization, build_string_list, security::DruidTlsSecurity, + DeepStorageSpec, DruidCluster, DruidRole, APP_NAME, AUTH_AUTHORIZER_OPA_URI, CERTS_DIR, + CREDENTIALS_SECRET_PROPERTY, DRUID_CONFIG_DIRECTORY, DS_BUCKET, EXTENSIONS_LOADLIST, + HDFS_CONFIG_DIRECTORY, JVM_CONFIG, LOG4J2_CONFIG, RUNTIME_PROPS, RW_CONFIG_DIRECTORY, + S3_ENDPOINT_URL, S3_PATH_STYLE_ACCESS, S3_SECRET_DIR_NAME, ZOOKEEPER_CONNECTION_STRING, }; use stackable_druid_crd::{ build_recommended_labels, resource::{self, RoleResource}, - tls::DruidTlsSettings, }; use stackable_operator::commons::product_image_selection::ResolvedProductImage; use stackable_operator::{ @@ -104,7 +104,7 @@ pub enum Error { source: stackable_operator::error::Error, }, #[snafu(display( - "Failed to get ZooKeeper discovery config map for cluster: {}", + "failed to get ZooKeeper discovery config map for cluster: {}", cm_name ))] GetZookeeperConnStringConfigMap { @@ -112,36 +112,29 @@ pub enum Error { cm_name: String, }, #[snafu(display( - "Failed to get OPA discovery config map and/or connection string for cluster: {}", + "failed to get OPA discovery config map and/or connection string for cluster: {}", cm_name ))] GetOpaConnString { source: stackable_operator::error::Error, cm_name: String, }, - #[snafu(display("Failed to get valid S3 connection"))] + #[snafu(display("failed to get valid S3 connection"))] GetS3Connection { source: stackable_druid_crd::Error }, - #[snafu(display("Failed to get deep storage bucket"))] + #[snafu(display("failed to get deep storage bucket"))] GetDeepStorageBucket { source: stackable_operator::error::Error, }, #[snafu(display( - "Failed to get ZooKeeper connection string from config map {}", + "failed to get ZooKeeper connection string from config map {}", cm_name ))] MissingZookeeperConnString { cm_name: String }, - #[snafu(display("Failed to get OPA discovery config map for cluster: {}", cm_name))] - GetOpaConnStringConfigMap { - source: stackable_operator::error::Error, - cm_name: String, - }, - #[snafu(display("Failed to get OPA connection string from config map {}", cm_name))] - MissingOpaConnString { cm_name: String }, - #[snafu(display("Failed to transform configs"))] + #[snafu(display("failed to transform configs"))] ProductConfigTransform { source: stackable_operator::product_config_utils::ConfigError, }, - #[snafu(display("Failed to format runtime properties"))] + #[snafu(display("failed to format runtime properties"))] PropertiesWriteError { source: stackable_operator::product_config::writer::PropertiesWriterError, }, @@ -184,20 +177,12 @@ pub enum Error { source: stackable_operator::error::Error, name: String, }, - #[snafu(display("no quantity unit (k, m, g, etc.) given for [{value}]"))] - NoQuantityUnit { value: String }, - #[snafu(display("invalid quantity value"))] - InvalidQuantityValue { source: std::num::ParseIntError }, - #[snafu(display("segment cache location is required but missing"))] - NoSegmentCacheLocation, - #[snafu(display("role group and resource type mismatch. this is a programming error."))] - RoleResourceMismatch, - #[snafu(display("invalid authentication configuration"))] - InvalidAuthenticationConfig { source: authentication::Error }, - #[snafu(display("invalid tls configuration"))] - InvalidTlsConfig { source: tls::Error }, #[snafu(display("object defines no namespace"))] ObjectHasNoNamespace, + #[snafu(display("failed to initialize security context"))] + FailedToInitializeSecurityContext { + source: stackable_druid_crd::security::Error, + }, } type Result = std::result::Result; @@ -271,30 +256,9 @@ pub async fn reconcile_druid(druid: Arc, ctx: Arc) -> Result< _ => None, }; - // Get possible authentication methods - let resolved_authentication_config = DruidAuthentication::resolve(client, &druid) + let druid_tls_security = DruidTlsSecurity::new_from_druid_cluster(client, &druid) .await - .context(InvalidAuthenticationConfigSnafu)?; - - let mut tls_authentication_config = None; - let mut authentication_config = vec![]; - // Remove a TLS provider if available. The TLS provider must be treated in combination - // with TLS encryption. Retain all other authentication providers. - for auth_conf in resolved_authentication_config.into_iter() { - if auth_conf.is_tls_auth() { - tls_authentication_config = Some(auth_conf); - } else { - authentication_config.push(auth_conf); - } - } - - // Extract TLS encryption and TLS auth provider if available - let druid_tls_settings = DruidTlsSettings { - encryption: druid.spec.cluster_config.tls.clone(), - // There can currently only be one TLS authentication config, so if we find it we can just - // take ownership. - authentication: tls_authentication_config, - }; + .context(FailedToInitializeSecurityContextSnafu)?; // False positive, auto-deref breaks type inference #[allow(clippy::explicit_auto_deref)] @@ -325,7 +289,7 @@ pub async fn reconcile_druid(druid: Arc, ctx: Arc) -> Result< &druid, &resolved_product_image, &druid_role, - &druid_tls_settings, + &druid_tls_security, )?; cluster_resources .add(client, &role_service) @@ -345,7 +309,7 @@ pub async fn reconcile_druid(druid: Arc, ctx: Arc) -> Result< &druid, &resolved_product_image, &rolegroup, - &druid_tls_settings, + &druid_tls_security, )?; let rg_configmap = build_rolegroup_config_map( &druid, @@ -357,7 +321,7 @@ pub async fn reconcile_druid(druid: Arc, ctx: Arc) -> Result< s3_conn.as_ref(), deep_storage_bucket_name.as_deref(), &resources, - &druid_tls_settings, + &druid_tls_security, )?; let rg_statefulset = build_rolegroup_statefulset( &druid, @@ -366,7 +330,7 @@ pub async fn reconcile_druid(druid: Arc, ctx: Arc) -> Result< rolegroup_config, s3_conn.as_ref(), &resources, - &druid_tls_settings, + &druid_tls_security, )?; cluster_resources .add(client, &rg_service) @@ -390,9 +354,14 @@ pub async fn reconcile_druid(druid: Arc, ctx: Arc) -> Result< } // discovery - for discovery_cm in build_discovery_configmaps(&druid, &*druid, &resolved_product_image) - .await - .context(BuildDiscoveryConfigSnafu)? + for discovery_cm in build_discovery_configmaps( + &druid, + &*druid, + &resolved_product_image, + &druid_tls_security, + ) + .await + .context(BuildDiscoveryConfigSnafu)? { cluster_resources .add(client, &discovery_cm) @@ -414,7 +383,7 @@ pub fn build_role_service( druid: &DruidCluster, resolved_product_image: &ResolvedProductImage, role: &DruidRole, - tls_settings: &DruidTlsSettings, + druid_tls_security: &DruidTlsSecurity, ) -> Result { let role_name = role.to_string(); let role_svc_name = format!( @@ -437,7 +406,7 @@ pub fn build_role_service( )) .build(), spec: Some(ServiceSpec { - ports: Some(tls_settings.service_ports(role)), + ports: Some(druid_tls_security.service_ports(role)), selector: Some(role_selector_labels(druid, APP_NAME, &role_name)), type_: Some("NodePort".to_string()), ..ServiceSpec::default() @@ -458,7 +427,7 @@ fn build_rolegroup_config_map( s3_conn: Option<&S3ConnectionSpec>, deep_storage_bucket_name: Option<&str>, resources: &RoleResource, - tls_settings: &DruidTlsSettings, + druid_tls_security: &DruidTlsSecurity, ) -> Result { let role = DruidRole::from_str(&rolegroup.role).unwrap(); let mut cm_conf_data = BTreeMap::new(); // filename -> filecontent @@ -483,12 +452,22 @@ fn build_rolegroup_config_map( ZOOKEEPER_CONNECTION_STRING.to_string(), Some(zk_connstr.to_string()), ); + + transformed_config.insert( + EXTENSIONS_LOADLIST.to_string(), + Some(build_string_list(&get_extension_list( + druid, + druid_tls_security, + ))), + ); + if let Some(opa_str) = opa_connstr { transformed_config.insert( AUTH_AUTHORIZER_OPA_URI.to_string(), Some(opa_str.to_string()), ); }; + if let Some(conn) = s3_conn { if let Some(endpoint) = conn.endpoint() { transformed_config.insert(S3_ENDPOINT_URL.to_string(), Some(endpoint)); @@ -510,7 +489,7 @@ fn build_rolegroup_config_map( ); // add tls encryption / auth properties - tls_settings.add_tls_config_properties(&mut transformed_config, &role); + druid_tls_security.add_tls_config_properties(&mut transformed_config, &role); let runtime_properties = stackable_operator::product_config::writer::to_java_properties_string( @@ -577,7 +556,7 @@ fn build_rolegroup_services( druid: &DruidCluster, resolved_product_image: &ResolvedProductImage, rolegroup: &RoleGroupRef, - tls_settings: &DruidTlsSettings, + druid_tls_security: &DruidTlsSecurity, ) -> Result { let role = DruidRole::from_str(&rolegroup.role).unwrap(); @@ -598,7 +577,7 @@ fn build_rolegroup_services( .build(), spec: Some(ServiceSpec { cluster_ip: Some("None".to_string()), - ports: Some(tls_settings.service_ports(&role)), + ports: Some(druid_tls_security.service_ports(&role)), selector: Some(role_group_selector_labels( druid, APP_NAME, @@ -622,7 +601,7 @@ fn build_rolegroup_statefulset( rolegroup_config: &HashMap>, s3_conn: Option<&S3ConnectionSpec>, resources: &RoleResource, - tls_settings: &DruidTlsSettings, + druid_tls_security: &DruidTlsSecurity, ) -> Result { let role = DruidRole::from_str(&rolegroup_ref.role).context(UnidentifiedDruidRoleSnafu { role: rolegroup_ref.role.to_string(), @@ -639,9 +618,9 @@ fn build_rolegroup_statefulset( pb.node_selector_opt(druid.node_selector(rolegroup_ref)); // volume and volume mounts - tls_settings + druid_tls_security .add_tls_volume_and_volume_mounts(&mut cb_prepare, &mut cb_druid, &mut pb) - .context(InvalidTlsConfigSnafu)?; + .context(FailedToInitializeSecurityContextSnafu)?; add_s3_volume_and_volume_mounts(s3_conn, &mut cb_druid, &mut pb)?; add_config_volume_and_volume_mounts(rolegroup_ref, &mut cb_druid, &mut pb); add_hdfs_cm_volume_and_volume_mounts( @@ -651,7 +630,7 @@ fn build_rolegroup_statefulset( ); resources.update_volumes_and_volume_mounts(&mut cb_druid, &mut pb); - let prepare_container_command = tls_settings.build_tls_key_stores_cmd(); + let prepare_container_command = druid_tls_security.build_tls_key_stores_cmd(); cb_prepare .image_from_product_image(resolved_product_image) @@ -676,13 +655,13 @@ fn build_rolegroup_statefulset( .image_from_product_image(resolved_product_image) .command(role.get_command(s3_conn)) .add_env_vars(rest_env) - .add_container_ports(tls_settings.container_ports(&role)) + .add_container_ports(druid_tls_security.container_ports(&role)) // 10s * 30 = 300s to come up - .startup_probe(tls_settings.get_tcp_socket_probe(30, 10, 30, 3)) + .startup_probe(druid_tls_security.get_tcp_socket_probe(30, 10, 30, 3)) // 10s * 1 = 10s to get removed from service - .readiness_probe(tls_settings.get_tcp_socket_probe(10, 10, 1, 3)) + .readiness_probe(druid_tls_security.get_tcp_socket_probe(10, 10, 1, 3)) // 10s * 3 = 30s to be restarted - .liveness_probe(tls_settings.get_tcp_socket_probe(10, 10, 3, 3)) + .liveness_probe(druid_tls_security.get_tcp_socket_probe(10, 10, 3, 3)) .resources(resources.as_resource_requirements()); pb.image_pull_secrets_from_product_image(resolved_product_image) @@ -826,7 +805,9 @@ pub fn error_policy(_obj: Arc, _error: &Error, _ctx: Arc) -> mod test { use super::*; use rstest::*; - use stackable_druid_crd::PROP_SEGMENT_CACHE_LOCATIONS; + use stackable_druid_crd::{ + authentication::ResolvedAuthenticationClasses, PROP_SEGMENT_CACHE_LOCATIONS, + }; use stackable_operator::product_config::{writer, ProductConfigManager}; #[derive(Snafu, Debug, EnumDiscriminants)] @@ -893,10 +874,10 @@ mod test { ) .context(OperatorFrameworkSnafu)?; - let druid_tls_settings = DruidTlsSettings { - encryption: druid.spec.cluster_config.tls.clone(), - authentication: None, - }; + let druid_tls_security = DruidTlsSecurity::new( + ResolvedAuthenticationClasses::new(vec![]), + Some("tls".to_string()), + ); let mut druid_segment_cache_property = "invalid".to_string(); @@ -925,7 +906,7 @@ mod test { None, None, &resources, - &druid_tls_settings, + &druid_tls_security, ) .context(ControllerSnafu)?; diff --git a/rust/operator-binary/src/extensions.rs b/rust/operator-binary/src/extensions.rs new file mode 100644 index 00000000..f604da85 --- /dev/null +++ b/rust/operator-binary/src/extensions.rs @@ -0,0 +1,42 @@ +use stackable_druid_crd::{security::DruidTlsSecurity, DbType, DruidCluster}; + +const EXT_S3: &str = "druid-s3-extensions"; +const EXT_KAFKA_INDEXING: &str = "druid-kafka-indexing-service"; +const EXT_DATASKETCHES: &str = "druid-datasketches"; +const PROMETHEUS_EMITTER: &str = "prometheus-emitter"; +const EXT_PSQL_MD_ST: &str = "postgresql-metadata-storage"; +const EXT_MYSQL_MD_ST: &str = "mysql-metadata-storage"; +const EXT_OPA_AUTHORIZER: &str = "druid-opa-authorizer"; +const EXT_BASIC_SECURITY: &str = "druid-basic-security"; +const EXT_HDFS: &str = "druid-hdfs-storage"; +const EXT_SIMPLE_CLIENT_SSL_CONTEXT: &str = "simple-client-sslcontext"; + +pub fn get_extension_list( + druid: &DruidCluster, + druid_tls_security: &DruidTlsSecurity, +) -> Vec { + let mut extensions = vec![ + EXT_KAFKA_INDEXING.to_string(), + EXT_DATASKETCHES.to_string(), + PROMETHEUS_EMITTER.to_string(), + EXT_BASIC_SECURITY.to_string(), + EXT_OPA_AUTHORIZER.to_string(), + EXT_HDFS.to_string(), + ]; + + match druid.spec.cluster_config.metadata_storage_database.db_type { + DbType::Derby => {} // no additional extensions required + DbType::Postgresql => extensions.push(EXT_PSQL_MD_ST.to_string()), + DbType::Mysql => extensions.push(EXT_MYSQL_MD_ST.to_string()), + } + + if druid_tls_security.tls_enabled() { + extensions.push(EXT_SIMPLE_CLIENT_SSL_CONTEXT.to_string()); + } + + if druid.uses_s3() { + extensions.push(EXT_S3.to_string()); + } + + extensions +} diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index e9c37c0d..50e74d19 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,6 +1,7 @@ mod config; mod discovery; mod druid_controller; +mod extensions; use std::sync::Arc; diff --git a/rust/operator-binary/test/resources/druid_controller/segment_cache.yaml b/rust/operator-binary/test/resources/druid_controller/segment_cache.yaml index 96db5358..fbe62bb5 100644 --- a/rust/operator-binary/test/resources/druid_controller/segment_cache.yaml +++ b/rust/operator-binary/test/resources/druid_controller/segment_cache.yaml @@ -29,7 +29,6 @@ spec: port: 5432 user: druid password: druid - tls: null zookeeperConfigMapName: psql-druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/authorizer/03-install-druid.yaml.j2 b/tests/templates/kuttl/authorizer/03-install-druid.yaml.j2 index e7f19db6..b4fcc71a 100644 --- a/tests/templates/kuttl/authorizer/03-install-druid.yaml.j2 +++ b/tests/templates/kuttl/authorizer/03-install-druid.yaml.j2 @@ -27,11 +27,10 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: configOverrides: - runtime.properties: + runtime.properties: &runtime-properties druid.auth.authenticatorChain: "[\"MyBasicMetadataAuthenticator\"]" druid.auth.authenticator.MyBasicMetadataAuthenticator.type: basic @@ -58,109 +57,25 @@ spec: replicas: 1 coordinators: configOverrides: - runtime.properties: - druid.auth.authenticatorChain: "[\"MyBasicMetadataAuthenticator\"]" - druid.auth.authenticator.MyBasicMetadataAuthenticator.type: basic - - # Default password for 'admin' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialAdminPassword: password1 - - # Default password for internal 'druid_system' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialInternalClientPassword: password2 - - # Uses the metadata store for storing users, you can use authentication API to create new users and grant permissions - druid.auth.authenticator.MyBasicMetadataAuthenticator.credentialsValidator.type: metadata - - # If true and the request credential doesn't exists in this credentials store, the request will proceed to next Authenticator in the chain. - druid.auth.authenticator.MyBasicMetadataAuthenticator.skipOnFailure: "false" - druid.auth.authenticator.MyBasicMetadataAuthenticator.authorizerName: OpaAuthorizer - - # Escalator - druid.escalator.type: basic - druid.escalator.internalClientUsername: druid_system - druid.escalator.internalClientPassword: password2 - druid.escalator.authorizerName: OpaAuthorizer + runtime.properties: *runtime-properties roleGroups: default: replicas: 1 historicals: configOverrides: - runtime.properties: - druid.auth.authenticatorChain: "[\"MyBasicMetadataAuthenticator\"]" - druid.auth.authenticator.MyBasicMetadataAuthenticator.type: basic - - # Default password for 'admin' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialAdminPassword: password1 - - # Default password for internal 'druid_system' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialInternalClientPassword: password2 - - # Uses the metadata store for storing users, you can use authentication API to create new users and grant permissions - druid.auth.authenticator.MyBasicMetadataAuthenticator.credentialsValidator.type: metadata - - # If true and the request credential doesn't exists in this credentials store, the request will proceed to next Authenticator in the chain. - druid.auth.authenticator.MyBasicMetadataAuthenticator.skipOnFailure: "false" - druid.auth.authenticator.MyBasicMetadataAuthenticator.authorizerName: OpaAuthorizer - - # Escalator - druid.escalator.type: basic - druid.escalator.internalClientUsername: druid_system - druid.escalator.internalClientPassword: password2 - druid.escalator.authorizerName: OpaAuthorizer + runtime.properties: *runtime-properties roleGroups: default: replicas: 1 middleManagers: configOverrides: - runtime.properties: - druid.auth.authenticatorChain: "[\"MyBasicMetadataAuthenticator\"]" - druid.auth.authenticator.MyBasicMetadataAuthenticator.type: basic - - # Default password for 'admin' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialAdminPassword: password1 - - # Default password for internal 'druid_system' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialInternalClientPassword: password2 - - # Uses the metadata store for storing users, you can use authentication API to create new users and grant permissions - druid.auth.authenticator.MyBasicMetadataAuthenticator.credentialsValidator.type: metadata - - # If true and the request credential doesn't exists in this credentials store, the request will proceed to next Authenticator in the chain. - druid.auth.authenticator.MyBasicMetadataAuthenticator.skipOnFailure: "false" - druid.auth.authenticator.MyBasicMetadataAuthenticator.authorizerName: OpaAuthorizer - - # Escalator - druid.escalator.type: basic - druid.escalator.internalClientUsername: druid_system - druid.escalator.internalClientPassword: password2 - druid.escalator.authorizerName: OpaAuthorizer + runtime.properties: *runtime-properties roleGroups: default: replicas: 1 routers: configOverrides: - runtime.properties: - druid.auth.authenticatorChain: "[\"MyBasicMetadataAuthenticator\"]" - druid.auth.authenticator.MyBasicMetadataAuthenticator.type: basic - - # Default password for 'admin' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialAdminPassword: password1 - - # Default password for internal 'druid_system' user, should be changed for production. - druid.auth.authenticator.MyBasicMetadataAuthenticator.initialInternalClientPassword: password2 - - # Uses the metadata store for storing users, you can use authentication API to create new users and grant permissions - druid.auth.authenticator.MyBasicMetadataAuthenticator.credentialsValidator.type: metadata - - # If true and the request credential doesn't exists in this credentials store, the request will proceed to next Authenticator in the chain. - druid.auth.authenticator.MyBasicMetadataAuthenticator.skipOnFailure: "false" - druid.auth.authenticator.MyBasicMetadataAuthenticator.authorizerName: OpaAuthorizer - - # Escalator - druid.escalator.type: basic - druid.escalator.internalClientUsername: druid_system - druid.escalator.internalClientPassword: password2 - druid.escalator.authorizerName: OpaAuthorizer + runtime.properties: *runtime-properties roleGroups: default: replicas: 1 diff --git a/tests/templates/kuttl/authorizer/05-assert.yaml b/tests/templates/kuttl/authorizer/05-assert.yaml index a3cc6c3f..1f6e20c9 100644 --- a/tests/templates/kuttl/authorizer/05-assert.yaml +++ b/tests/templates/kuttl/authorizer/05-assert.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/authcheck.py + - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/authcheck.py derby-druid timeout: 600 diff --git a/tests/templates/kuttl/authorizer/05-authcheck.yaml b/tests/templates/kuttl/authorizer/05-authcheck.yaml index 719f96a9..585fa123 100644 --- a/tests/templates/kuttl/authorizer/05-authcheck.yaml +++ b/tests/templates/kuttl/authorizer/05-authcheck.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./authcheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ./authcheck.py checks-0:/tmp diff --git a/tests/templates/kuttl/authorizer/authcheck.py b/tests/templates/kuttl/authorizer/authcheck.py index d6cab56b..ae62af80 100755 --- a/tests/templates/kuttl/authorizer/authcheck.py +++ b/tests/templates/kuttl/authorizer/authcheck.py @@ -3,20 +3,27 @@ import logging coordinator_host = "derby-druid-coordinator-default" -coordinator_port = "8081" +coordinator_port = "8281" authenticator_name = "MyBasicMetadataAuthenticator" def create_user(user_name): requests.post( - f"http://{coordinator_host}:{coordinator_port}/druid-ext/basic-security/authentication/db/{authenticator_name}/users/{user_name}", - auth=("admin", "password1") + f"https://{coordinator_host}:{coordinator_port}/druid-ext/basic-security/authentication/db/{authenticator_name}/users/{user_name}", + auth=("admin", "password1"), + verify=False, ) data = f"{{\"password\": \"{user_name}\"}}" headers = { 'Content-Type': 'application/json', } - requests.post(f"http://{coordinator_host}:{coordinator_port}/druid-ext/basic-security/authentication/db/{authenticator_name}/users/{user_name}/credentials", headers=headers, data=data, auth=('admin', 'password1')) + requests.post( + f"https://{coordinator_host}:{coordinator_port}/druid-ext/basic-security/authentication/db/{authenticator_name}/users/{user_name}/credentials", + headers=headers, + data=data, + auth=('admin', 'password1'), + verify=False, + ) if __name__ == "__main__": @@ -30,39 +37,33 @@ def create_user(user_name): create_user("eve") print("USERS CREATED!") - druid_cluster_name = "derby-druid" - druid_roles = [ - "broker", - "coordinator", - "middlemanager", - "historical", - "router" - ] - druid_ports = { - "broker": 8082, - "coordinator": 8081, - "middlemanager": 8091, - "historical": 8083, - "router": 8888 + druid_cluster_name = sys.argv[1] + + druid_role_ports = { + "broker": 8282, + "coordinator": 8281, + "middlemanager": 8291, + "historical": 8283, + "router": 9088, } - for role in druid_roles: - url = f"http://{druid_cluster_name}-{role}-default:{druid_ports[role]}/status" + for role, port in druid_role_ports.items(): + url = f"https://{druid_cluster_name}-{role}-default:{port}/status" # make an authorized request -> return 401 expected print("Checking Unauthorized") - res = requests.get(url) + res = requests.get(url, verify=False) if res.status_code != 401: result = 1 break # make an authorized request -> return 200 expected print("Checking Alice") - res = requests.get(url, auth=("alice", "alice")) + res = requests.get(url, auth=("alice", "alice"), verify=False) if res.status_code != 200: result = 1 break # make an unauthorized request -> return 403 expected print("Checking Eve") - res = requests.get(url, auth=("eve", "eve")) + res = requests.get(url, auth=("eve", "eve"), verify=False) if res.status_code != 403: result = 1 break diff --git a/tests/templates/kuttl/hdfs-deep-storage/druid-quickstartimport.json b/tests/templates/kuttl/commons/druid-quickstartimport.json similarity index 100% rename from tests/templates/kuttl/hdfs-deep-storage/druid-quickstartimport.json rename to tests/templates/kuttl/commons/druid-quickstartimport.json diff --git a/tests/templates/kuttl/s3-deep-storage/healthcheck.py b/tests/templates/kuttl/commons/healthcheck.py similarity index 82% rename from tests/templates/kuttl/s3-deep-storage/healthcheck.py rename to tests/templates/kuttl/commons/healthcheck.py index 95bcea0b..96815fd8 100755 --- a/tests/templates/kuttl/s3-deep-storage/healthcheck.py +++ b/tests/templates/kuttl/commons/healthcheck.py @@ -11,23 +11,16 @@ druid_cluster_name = sys.argv[1] - druid_roles = [ - "broker", - "coordinator", - "middlemanager", - "historical", - "router" - ] - druid_ports = { - "broker": 8082, - "coordinator": 8081, - "middlemanager": 8091, - "historical": 8083, - "router": 8888 + druid_role_ports = { + "broker": 8282, + "coordinator": 8281, + "middlemanager": 8291, + "historical": 8283, + "router": 9088, } - for role in druid_roles: - url = f"http://{druid_cluster_name}-{role}-default:{druid_ports[role]}/status/health" + for role, port in druid_role_ports.items(): + url = f"https://{druid_cluster_name}-{role}-default:{port}/status/health" count = 1 # As this script is intended to be executed by Kuttl which is in charge of overall test timeouts it is ok @@ -44,7 +37,7 @@ try: count = count + 1 print(f"Checking role [{role}] on url [{url}]") - res = requests.get(url, timeout=5) + res = requests.get(url, verify=False, timeout=5) code = res.status_code if res.status_code == 200 and res.text.lower() == "true": break diff --git a/tests/templates/kuttl/tls/ingestioncheck.py b/tests/templates/kuttl/commons/ingestioncheck-tls.py similarity index 92% rename from tests/templates/kuttl/tls/ingestioncheck.py rename to tests/templates/kuttl/commons/ingestioncheck-tls.py index 73bc6cd4..9c8d5040 100755 --- a/tests/templates/kuttl/tls/ingestioncheck.py +++ b/tests/templates/kuttl/commons/ingestioncheck-tls.py @@ -52,26 +52,26 @@ def query_datasource(self, url, sql, expected, iterations): druid_cluster_name = sys.argv[2] security = sys.argv[3] -if security == "insecure": +if security == "no-tls": protocol = "http" coordinator_port = "8081" broker_port = "8082" cert = None verify = False -elif security == "secure": +elif security == "internal-and-server-tls": protocol = "https" coordinator_port = "8281" broker_port = "8282" cert = None - verify = "/tmp/tls/ca.crt" -elif security == "secure_auth": + verify = "/tmp/druid-tls/ca.crt" +elif security == "internal-and-server-tls-and-tls-client-auth": protocol = "https" coordinator_port = "8281" broker_port = "8282" - cert = ("/tmp/tls_auth/tls.crt", "/tmp/tls_auth/tls.key") - verify = "/tmp/tls_auth/ca.crt" + cert = ("/tmp/druid-tls/tls.crt", "/tmp/druid-tls/tls.key") + verify = "/tmp/druid-tls/ca.crt" else: - sys.exit("Usage: python ./ingestioncheck.py ") + sys.exit("Usage: python ./ingestioncheck.py ") druid = DruidClient(cert, verify) diff --git a/tests/templates/kuttl/ingestion-s3-ext/ingestioncheck.py b/tests/templates/kuttl/commons/ingestioncheck.py similarity index 83% rename from tests/templates/kuttl/ingestion-s3-ext/ingestioncheck.py rename to tests/templates/kuttl/commons/ingestioncheck.py index dd0c1b9e..2269b5d6 100755 --- a/tests/templates/kuttl/ingestion-s3-ext/ingestioncheck.py +++ b/tests/templates/kuttl/commons/ingestioncheck.py @@ -11,6 +11,7 @@ class DruidClient: def __init__(self): self.session = requests.Session() self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) + self.session.verify = False http.client.HTTPConnection.debuglevel = 1 def get(self, url): @@ -52,7 +53,7 @@ def query_datasource(self, url, sql, expected, iterations): Query tasks ===========''') tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", + url=f"https://{druid_cluster_name}-coordinator-default:8281/druid/indexer/v1/tasks", ) task_count = len(json.loads(tasks)) print(f'existing tasks: {task_count}') @@ -61,7 +62,7 @@ def query_datasource(self, url, sql, expected, iterations): Start ingestion task ====================''') ingestion = druid.post_task( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task", + url=f"https://{druid_cluster_name}-coordinator-default:8281/druid/indexer/v1/task", input='/tmp/druid-quickstartimport.json' ) task_id = json.loads(ingestion)["task"] @@ -71,7 +72,7 @@ def query_datasource(self, url, sql, expected, iterations): Re-query tasks ==============''') tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", + url=f"https://{druid_cluster_name}-coordinator-default:8281/druid/indexer/v1/tasks", ) new_task_count = len(json.loads(tasks)) print(f'new tasks: {new_task_count}') @@ -85,7 +86,7 @@ def query_datasource(self, url, sql, expected, iterations): while not job_finished: time.sleep(5) task = druid.get( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task/{url_encoded_taskid}/status", + url=f"https://{druid_cluster_name}-coordinator-default:8281/druid/indexer/v1/task/{url_encoded_taskid}/status", ) task_status = json.loads(task)["status"]["statusCode"] print(f"Current task status: [{task_status}]") @@ -98,7 +99,7 @@ def query_datasource(self, url, sql, expected, iterations): broker_ready = False while not broker_ready: time.sleep(2) - broker_ready_rc = druid.check_rc(f"http://{druid_cluster_name}-broker-default:8082/druid/broker/v1/readiness") + broker_ready_rc = druid.check_rc(f"https://{druid_cluster_name}-broker-default:8282/druid/broker/v1/readiness") broker_ready = broker_ready_rc == 200 print(f"Broker respondend with [{broker_ready_rc}] to readiness check") @@ -107,7 +108,7 @@ def query_datasource(self, url, sql, expected, iterations): ==============''') sample_data_size = 39244 result = druid.query_datasource( - url=f"http://{druid_cluster_name}-broker-default:8082/druid/v2/sql", + url=f"https://{druid_cluster_name}-broker-default:8282/druid/v2/sql", sql={"query": "select count(*) as c from \"wikipedia-2015-09-12\""}, expected=sample_data_size, iterations=12 diff --git a/tests/templates/kuttl/hdfs-deep-storage/02-install-druid.yaml.j2 b/tests/templates/kuttl/hdfs-deep-storage/02-install-druid.yaml.j2 index 9efba39c..1de84747 100644 --- a/tests/templates/kuttl/hdfs-deep-storage/02-install-druid.yaml.j2 +++ b/tests/templates/kuttl/hdfs-deep-storage/02-install-druid.yaml.j2 @@ -23,7 +23,6 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/hdfs-deep-storage/04-assert.yaml b/tests/templates/kuttl/hdfs-deep-storage/04-assert.yaml index bf22ffe7..07a25600 100644 --- a/tests/templates/kuttl/hdfs-deep-storage/04-assert.yaml +++ b/tests/templates/kuttl/hdfs-deep-storage/04-assert.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py + - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py derby-druid timeout: 300 diff --git a/tests/templates/kuttl/hdfs-deep-storage/04-healthcheck.yaml b/tests/templates/kuttl/hdfs-deep-storage/04-healthcheck.yaml index 4738b350..bb53960e 100644 --- a/tests/templates/kuttl/hdfs-deep-storage/04-healthcheck.yaml +++ b/tests/templates/kuttl/hdfs-deep-storage/04-healthcheck.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./healthcheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/healthcheck.py checks-0:/tmp diff --git a/tests/templates/kuttl/hdfs-deep-storage/05-ingestioncheck.yaml b/tests/templates/kuttl/hdfs-deep-storage/05-ingestioncheck.yaml index c5b501e7..f9165a35 100644 --- a/tests/templates/kuttl/hdfs-deep-storage/05-ingestioncheck.yaml +++ b/tests/templates/kuttl/hdfs-deep-storage/05-ingestioncheck.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: - - script: kubectl cp -n $NAMESPACE ./ingestioncheck.py checks-0:/tmp - - script: kubectl cp -n $NAMESPACE ./druid-quickstartimport.json checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/ingestioncheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/druid-quickstartimport.json checks-0:/tmp diff --git a/tests/templates/kuttl/hdfs-deep-storage/healthcheck.py b/tests/templates/kuttl/hdfs-deep-storage/healthcheck.py deleted file mode 100755 index 3136b8a5..00000000 --- a/tests/templates/kuttl/hdfs-deep-storage/healthcheck.py +++ /dev/null @@ -1,63 +0,0 @@ -import requests -import sys -import logging -import time - -if __name__ == "__main__": - result = 0 - - log_level = 'DEBUG' # if args.debug else 'INFO' - logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s: %(message)s', stream=sys.stdout) - - druid_cluster_name = "derby-druid" - druid_roles = [ - "broker", - "coordinator", - "middlemanager", - "historical", - "router" - ] - druid_ports = { - "broker": 8082, - "coordinator": 8081, - "middlemanager": 8091, - "historical": 8083, - "router": 8888 - } - - for role in druid_roles: - url = f"http://{druid_cluster_name}-{role}-default:{druid_ports[role]}/status/health" - count = 1 - - # As this script is intended to be executed by Kuttl which is in charge of overall test timeouts it is ok - # to loop infinitely here - or until all tests succeed - # The script iterates over all known ports and services and checks that the ports are available - # The timeout for this connection attempt is configured to 5 seconds, to ensure frequent retries that are - # not handled internally by the requests library, because it was unclear when or if dns entries are cached - # internally during retry handling. - # By issuing a new call to .get() we are trying to ensure a new dns lookup for the target. - # - # Any errors are logged and retried until either the test succeeds or Kuttl kills this script due to - # the timeout. - while True: - try: - count = count + 1 - print(f"Checking role [{role}] on url [{url}]") - res = requests.get(url, timeout=5) - code = res.status_code - if res.status_code == 200 and res.text.lower() == "true": - break - else: - print(f"Got non 200 status code [{res.status_code}] or non-true response [{res.text.lower()}], retrying attempt no [{count}] ....") - except requests.exceptions.Timeout: - print(f"Connection timed out, retrying attempt no [{count}] ....") - except requests.ConnectionError as e: - print(f"Connection Error: {str(e)}") - except requests.RequestException as e: - print(f"General Error: {str(e)}") - except Exception: - print(f"Unhandled error occurred, retrying attempt no [{count}] ....") - - # Wait a little bit before retrying - time.sleep(1) - sys.exit(0) diff --git a/tests/templates/kuttl/hdfs-deep-storage/ingestioncheck.py b/tests/templates/kuttl/hdfs-deep-storage/ingestioncheck.py deleted file mode 100755 index dd0c1b9e..00000000 --- a/tests/templates/kuttl/hdfs-deep-storage/ingestioncheck.py +++ /dev/null @@ -1,117 +0,0 @@ -import urllib - -import requests -import http -import sys -import json -import time - - -class DruidClient: - def __init__(self): - self.session = requests.Session() - self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) - http.client.HTTPConnection.debuglevel = 1 - - def get(self, url): - response = self.session.get(url) - assert response.status_code == 200 - return response.text - - def get_tasks(self, url): - response = self.session.get(url) - assert response.status_code == 200 - return response.text - - def post_task(self, url, input): - response = self.session.post(url, data=open(input, 'rb')) - assert response.status_code == 200 - return response.text - - def check_rc(self, url): - response = self.session.get(url) - return response.status_code - - def query_datasource(self, url, sql, expected, iterations): - loop = 0 - while True: - response = self.session.post(url, json=sql) - assert response.status_code == 200 - actual = list(json.loads(response.text)[0].values())[0] - if (actual == expected) | (loop == iterations): - break - time.sleep(5) - loop += 1 - return actual - - -druid_cluster_name = sys.argv[1] -druid = DruidClient() - -print(''' -Query tasks -===========''') -tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", -) -task_count = len(json.loads(tasks)) -print(f'existing tasks: {task_count}') - -print(''' -Start ingestion task -====================''') -ingestion = druid.post_task( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task", - input='/tmp/druid-quickstartimport.json' -) -task_id = json.loads(ingestion)["task"] -url_encoded_taskid = urllib.parse.quote(task_id, safe='') -print(f"TASKID: [{task_id}]") -print(''' -Re-query tasks -==============''') -tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", -) -new_task_count = len(json.loads(tasks)) -print(f'new tasks: {new_task_count}') -print(f'assert {new_task_count} == {task_count+1}') -assert new_task_count == task_count + 1 - -print(''' -Wait for ingestion task to succeed -======================================''') -job_finished = False -while not job_finished: - time.sleep(5) - task = druid.get( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task/{url_encoded_taskid}/status", - ) - task_status = json.loads(task)["status"]["statusCode"] - print(f"Current task status: [{task_status}]") - assert task_status == "RUNNING" or task_status == "SUCCESS", f"Taskstatus not running or succeeeded: {task_status}" - job_finished = task_status == "SUCCESS" - -print(''' -Wait for broker to indicate all segments are fully online -======================================''') -broker_ready = False -while not broker_ready: - time.sleep(2) - broker_ready_rc = druid.check_rc(f"http://{druid_cluster_name}-broker-default:8082/druid/broker/v1/readiness") - broker_ready = broker_ready_rc == 200 - print(f"Broker respondend with [{broker_ready_rc}] to readiness check") - -print(''' -Datasource SQL -==============''') -sample_data_size = 39244 -result = druid.query_datasource( - url=f"http://{druid_cluster_name}-broker-default:8082/druid/v2/sql", - sql={"query": "select count(*) as c from \"wikipedia-2015-09-12\""}, - expected=sample_data_size, - iterations=12 -) -print(f'results: {result}') -print(f'assert {sample_data_size} == {result}') -assert sample_data_size == result diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/02-install-druid.yaml.j2 b/tests/templates/kuttl/ingestion-no-s3-ext/02-install-druid.yaml.j2 index 9efba39c..1de84747 100644 --- a/tests/templates/kuttl/ingestion-no-s3-ext/02-install-druid.yaml.j2 +++ b/tests/templates/kuttl/ingestion-no-s3-ext/02-install-druid.yaml.j2 @@ -23,7 +23,6 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/04-assert.yaml b/tests/templates/kuttl/ingestion-no-s3-ext/04-assert.yaml index bf22ffe7..07a25600 100644 --- a/tests/templates/kuttl/ingestion-no-s3-ext/04-assert.yaml +++ b/tests/templates/kuttl/ingestion-no-s3-ext/04-assert.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py + - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py derby-druid timeout: 300 diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/04-healthcheck.yaml b/tests/templates/kuttl/ingestion-no-s3-ext/04-healthcheck.yaml index 4738b350..bb53960e 100644 --- a/tests/templates/kuttl/ingestion-no-s3-ext/04-healthcheck.yaml +++ b/tests/templates/kuttl/ingestion-no-s3-ext/04-healthcheck.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./healthcheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/healthcheck.py checks-0:/tmp diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/05-ingestioncheck.yaml b/tests/templates/kuttl/ingestion-no-s3-ext/05-ingestioncheck.yaml index c5b501e7..f9165a35 100644 --- a/tests/templates/kuttl/ingestion-no-s3-ext/05-ingestioncheck.yaml +++ b/tests/templates/kuttl/ingestion-no-s3-ext/05-ingestioncheck.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: - - script: kubectl cp -n $NAMESPACE ./ingestioncheck.py checks-0:/tmp - - script: kubectl cp -n $NAMESPACE ./druid-quickstartimport.json checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/ingestioncheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/druid-quickstartimport.json checks-0:/tmp diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/druid-quickstartimport.json b/tests/templates/kuttl/ingestion-no-s3-ext/druid-quickstartimport.json deleted file mode 100644 index 909b9008..00000000 --- a/tests/templates/kuttl/ingestion-no-s3-ext/druid-quickstartimport.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "type": "index_parallel", - "spec": { - "ioConfig": { - "type": "index_parallel", - "inputSource": { - "type": "local", - "baseDir": "quickstart/tutorial/", - "filter": "wikiticker-2015-09-12-sampled.json.gz" - }, - "inputFormat": { - "type": "json" - } - }, - "tuningConfig": { - "type": "index_parallel", - "partitionsSpec": { - "type": "dynamic" - } - }, - "dataSchema": { - "dataSource": "wikipedia-2015-09-12", - "timestampSpec": { - "column": "time", - "format": "iso" - }, - "dimensionsSpec": { - "dimensions": [ - "channel", - "cityName", - "comment", - "countryIsoCode", - "countryName", - "isAnonymous", - "isMinor", - "isNew", - "isRobot", - "isUnpatrolled", - "metroCode", - "namespace", - "page", - "regionIsoCode", - "regionName", - "user", - { - "type": "long", - "name": "delta" - }, - { - "type": "long", - "name": "added" - }, - { - "type": "long", - "name": "deleted" - } - ] - }, - "granularitySpec": { - "queryGranularity": "none", - "rollup": false, - "segmentGranularity": "day" - } - } - } -} diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/healthcheck.py b/tests/templates/kuttl/ingestion-no-s3-ext/healthcheck.py deleted file mode 100755 index 4bf19cfc..00000000 --- a/tests/templates/kuttl/ingestion-no-s3-ext/healthcheck.py +++ /dev/null @@ -1,64 +0,0 @@ -import requests -import sys -import logging -import time - -if __name__ == "__main__": - result = 0 - - log_level = 'DEBUG' # if args.debug else 'INFO' - logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s: %(message)s', stream=sys.stdout) - - druid_cluster_name = "derby-druid" - druid_roles = [ - "broker", - "coordinator", - "middlemanager", - "historical", - "router" - ] - druid_ports = { - "broker": 8082, - "coordinator": 8081, - "middlemanager": 8091, - "historical": 8083, - "router": 8888 - } - - for role in druid_roles: - url = f"http://{druid_cluster_name}-{role}-default:{druid_ports[role]}/status/health" - count = 1 - - # As this script is intended to be executed by Kuttl which is in charge of overall test timeouts it is ok - # to loop infinitely here - or until all tests succeed - # The script iterates over all known ports and services and checks that the ports are available - # The timeout for this connection attempt is configured to 5 seconds, to ensure frequent retries that are - # not handled internally by the requests library, because it was unclear when or if dns entries are cached - # internally during retry handling. - # By issuing a new call to .get() we are trying to ensure a new dns lookup for the target. - # - # Any errors are logged and retried until either the test succeeds or Kuttl kills this script due to - # the timeout. - while True: - try: - count = count + 1 - print(f"Checking role [{role}] on url [{url}]") - res = requests.get(url, timeout=5) - code = res.status_code - if res.status_code == 200 and res.text.lower() == "true": - break - else: - print(f"Got non 200 status code [{res.status_code}] or non-true response [{res.text.lower()}], retrying attempt no [{count}] ....") - except requests.exceptions.Timeout: - print(f"Connection timed out, retrying attempt no [{count}] ....") - except requests.ConnectionError as e: - print(f"Connection Error: {str(e)}") - except requests.RequestException as e: - print(f"General Error: {str(e)}") - except Exception: - print(f"Unhandled error occurred, retrying attempt no [{count}] ....") - - # Wait a little bit before retrying - time.sleep(1) - - sys.exit(0) diff --git a/tests/templates/kuttl/ingestion-no-s3-ext/ingestioncheck.py b/tests/templates/kuttl/ingestion-no-s3-ext/ingestioncheck.py deleted file mode 100755 index dd0c1b9e..00000000 --- a/tests/templates/kuttl/ingestion-no-s3-ext/ingestioncheck.py +++ /dev/null @@ -1,117 +0,0 @@ -import urllib - -import requests -import http -import sys -import json -import time - - -class DruidClient: - def __init__(self): - self.session = requests.Session() - self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) - http.client.HTTPConnection.debuglevel = 1 - - def get(self, url): - response = self.session.get(url) - assert response.status_code == 200 - return response.text - - def get_tasks(self, url): - response = self.session.get(url) - assert response.status_code == 200 - return response.text - - def post_task(self, url, input): - response = self.session.post(url, data=open(input, 'rb')) - assert response.status_code == 200 - return response.text - - def check_rc(self, url): - response = self.session.get(url) - return response.status_code - - def query_datasource(self, url, sql, expected, iterations): - loop = 0 - while True: - response = self.session.post(url, json=sql) - assert response.status_code == 200 - actual = list(json.loads(response.text)[0].values())[0] - if (actual == expected) | (loop == iterations): - break - time.sleep(5) - loop += 1 - return actual - - -druid_cluster_name = sys.argv[1] -druid = DruidClient() - -print(''' -Query tasks -===========''') -tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", -) -task_count = len(json.loads(tasks)) -print(f'existing tasks: {task_count}') - -print(''' -Start ingestion task -====================''') -ingestion = druid.post_task( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task", - input='/tmp/druid-quickstartimport.json' -) -task_id = json.loads(ingestion)["task"] -url_encoded_taskid = urllib.parse.quote(task_id, safe='') -print(f"TASKID: [{task_id}]") -print(''' -Re-query tasks -==============''') -tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", -) -new_task_count = len(json.loads(tasks)) -print(f'new tasks: {new_task_count}') -print(f'assert {new_task_count} == {task_count+1}') -assert new_task_count == task_count + 1 - -print(''' -Wait for ingestion task to succeed -======================================''') -job_finished = False -while not job_finished: - time.sleep(5) - task = druid.get( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task/{url_encoded_taskid}/status", - ) - task_status = json.loads(task)["status"]["statusCode"] - print(f"Current task status: [{task_status}]") - assert task_status == "RUNNING" or task_status == "SUCCESS", f"Taskstatus not running or succeeeded: {task_status}" - job_finished = task_status == "SUCCESS" - -print(''' -Wait for broker to indicate all segments are fully online -======================================''') -broker_ready = False -while not broker_ready: - time.sleep(2) - broker_ready_rc = druid.check_rc(f"http://{druid_cluster_name}-broker-default:8082/druid/broker/v1/readiness") - broker_ready = broker_ready_rc == 200 - print(f"Broker respondend with [{broker_ready_rc}] to readiness check") - -print(''' -Datasource SQL -==============''') -sample_data_size = 39244 -result = druid.query_datasource( - url=f"http://{druid_cluster_name}-broker-default:8082/druid/v2/sql", - sql={"query": "select count(*) as c from \"wikipedia-2015-09-12\""}, - expected=sample_data_size, - iterations=12 -) -print(f'results: {result}') -print(f'assert {sample_data_size} == {result}') -assert sample_data_size == result diff --git a/tests/templates/kuttl/ingestion-s3-ext/02-install-druid.yaml.j2 b/tests/templates/kuttl/ingestion-s3-ext/02-install-druid.yaml.j2 index 9a34367a..4e6ba1da 100644 --- a/tests/templates/kuttl/ingestion-s3-ext/02-install-druid.yaml.j2 +++ b/tests/templates/kuttl/ingestion-s3-ext/02-install-druid.yaml.j2 @@ -29,7 +29,6 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/ingestion-s3-ext/04-assert.yaml b/tests/templates/kuttl/ingestion-s3-ext/04-assert.yaml index bf22ffe7..07a25600 100644 --- a/tests/templates/kuttl/ingestion-s3-ext/04-assert.yaml +++ b/tests/templates/kuttl/ingestion-s3-ext/04-assert.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py + - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py derby-druid timeout: 300 diff --git a/tests/templates/kuttl/ingestion-s3-ext/04-healthcheck.yaml b/tests/templates/kuttl/ingestion-s3-ext/04-healthcheck.yaml index 4738b350..bb53960e 100644 --- a/tests/templates/kuttl/ingestion-s3-ext/04-healthcheck.yaml +++ b/tests/templates/kuttl/ingestion-s3-ext/04-healthcheck.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./healthcheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/healthcheck.py checks-0:/tmp diff --git a/tests/templates/kuttl/ingestion-s3-ext/05-ingestioncheck.yaml b/tests/templates/kuttl/ingestion-s3-ext/05-ingestioncheck.yaml index c5b501e7..f9165a35 100644 --- a/tests/templates/kuttl/ingestion-s3-ext/05-ingestioncheck.yaml +++ b/tests/templates/kuttl/ingestion-s3-ext/05-ingestioncheck.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: - - script: kubectl cp -n $NAMESPACE ./ingestioncheck.py checks-0:/tmp - - script: kubectl cp -n $NAMESPACE ./druid-quickstartimport.json checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/ingestioncheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/druid-quickstartimport.json checks-0:/tmp diff --git a/tests/templates/kuttl/ingestion-s3-ext/druid-quickstartimport.json b/tests/templates/kuttl/ingestion-s3-ext/druid-quickstartimport.json deleted file mode 100644 index 909b9008..00000000 --- a/tests/templates/kuttl/ingestion-s3-ext/druid-quickstartimport.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "type": "index_parallel", - "spec": { - "ioConfig": { - "type": "index_parallel", - "inputSource": { - "type": "local", - "baseDir": "quickstart/tutorial/", - "filter": "wikiticker-2015-09-12-sampled.json.gz" - }, - "inputFormat": { - "type": "json" - } - }, - "tuningConfig": { - "type": "index_parallel", - "partitionsSpec": { - "type": "dynamic" - } - }, - "dataSchema": { - "dataSource": "wikipedia-2015-09-12", - "timestampSpec": { - "column": "time", - "format": "iso" - }, - "dimensionsSpec": { - "dimensions": [ - "channel", - "cityName", - "comment", - "countryIsoCode", - "countryName", - "isAnonymous", - "isMinor", - "isNew", - "isRobot", - "isUnpatrolled", - "metroCode", - "namespace", - "page", - "regionIsoCode", - "regionName", - "user", - { - "type": "long", - "name": "delta" - }, - { - "type": "long", - "name": "added" - }, - { - "type": "long", - "name": "deleted" - } - ] - }, - "granularitySpec": { - "queryGranularity": "none", - "rollup": false, - "segmentGranularity": "day" - } - } - } -} diff --git a/tests/templates/kuttl/ingestion-s3-ext/healthcheck.py b/tests/templates/kuttl/ingestion-s3-ext/healthcheck.py deleted file mode 100755 index 3136b8a5..00000000 --- a/tests/templates/kuttl/ingestion-s3-ext/healthcheck.py +++ /dev/null @@ -1,63 +0,0 @@ -import requests -import sys -import logging -import time - -if __name__ == "__main__": - result = 0 - - log_level = 'DEBUG' # if args.debug else 'INFO' - logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s: %(message)s', stream=sys.stdout) - - druid_cluster_name = "derby-druid" - druid_roles = [ - "broker", - "coordinator", - "middlemanager", - "historical", - "router" - ] - druid_ports = { - "broker": 8082, - "coordinator": 8081, - "middlemanager": 8091, - "historical": 8083, - "router": 8888 - } - - for role in druid_roles: - url = f"http://{druid_cluster_name}-{role}-default:{druid_ports[role]}/status/health" - count = 1 - - # As this script is intended to be executed by Kuttl which is in charge of overall test timeouts it is ok - # to loop infinitely here - or until all tests succeed - # The script iterates over all known ports and services and checks that the ports are available - # The timeout for this connection attempt is configured to 5 seconds, to ensure frequent retries that are - # not handled internally by the requests library, because it was unclear when or if dns entries are cached - # internally during retry handling. - # By issuing a new call to .get() we are trying to ensure a new dns lookup for the target. - # - # Any errors are logged and retried until either the test succeeds or Kuttl kills this script due to - # the timeout. - while True: - try: - count = count + 1 - print(f"Checking role [{role}] on url [{url}]") - res = requests.get(url, timeout=5) - code = res.status_code - if res.status_code == 200 and res.text.lower() == "true": - break - else: - print(f"Got non 200 status code [{res.status_code}] or non-true response [{res.text.lower()}], retrying attempt no [{count}] ....") - except requests.exceptions.Timeout: - print(f"Connection timed out, retrying attempt no [{count}] ....") - except requests.ConnectionError as e: - print(f"Connection Error: {str(e)}") - except requests.RequestException as e: - print(f"General Error: {str(e)}") - except Exception: - print(f"Unhandled error occurred, retrying attempt no [{count}] ....") - - # Wait a little bit before retrying - time.sleep(1) - sys.exit(0) diff --git a/tests/templates/kuttl/orphaned-resources/02-install-druid.yaml.j2 b/tests/templates/kuttl/orphaned-resources/02-install-druid.yaml.j2 index 9efba39c..1de84747 100644 --- a/tests/templates/kuttl/orphaned-resources/02-install-druid.yaml.j2 +++ b/tests/templates/kuttl/orphaned-resources/02-install-druid.yaml.j2 @@ -23,7 +23,6 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/resources/20-assert.yaml b/tests/templates/kuttl/resources/20-assert.yaml index 65d0b244..dff64083 100644 --- a/tests/templates/kuttl/resources/20-assert.yaml +++ b/tests/templates/kuttl/resources/20-assert.yaml @@ -60,14 +60,26 @@ spec: cpu: "4" memory: 2Gi volumes: - - configMap: + - name: tls-mount + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: pod,node + creationTimestamp: null + spec: + storageClassName: secrets.stackable.tech + - name: tls + emptyDir: {} + - name: config + configMap: name: druid-resources-historical-default - name: config - - emptyDir: {} - name: rwconfig - - emptyDir: + - name: rwconfig + emptyDir: {} + - name: segment-cache + emptyDir: sizeLimit: 2G - name: segment-cache status: readyReplicas: 1 replicas: 1 diff --git a/tests/templates/kuttl/resources/20-install-druid.yaml.j2 b/tests/templates/kuttl/resources/20-install-druid.yaml.j2 index 32326e48..dce0a12f 100644 --- a/tests/templates/kuttl/resources/20-install-druid.yaml.j2 +++ b/tests/templates/kuttl/resources/20-install-druid.yaml.j2 @@ -27,7 +27,6 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/s3-deep-storage/10-install-druid.yaml.j2 b/tests/templates/kuttl/s3-deep-storage/10-install-druid.yaml.j2 index 051a856d..a06c63d4 100644 --- a/tests/templates/kuttl/s3-deep-storage/10-install-druid.yaml.j2 +++ b/tests/templates/kuttl/s3-deep-storage/10-install-druid.yaml.j2 @@ -59,7 +59,6 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/s3-deep-storage/11-healthcheck.yaml b/tests/templates/kuttl/s3-deep-storage/11-healthcheck.yaml index 1a3627a5..bb53960e 100644 --- a/tests/templates/kuttl/s3-deep-storage/11-healthcheck.yaml +++ b/tests/templates/kuttl/s3-deep-storage/11-healthcheck.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./healthcheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/healthcheck.py checks-0:/tmp diff --git a/tests/templates/kuttl/s3-deep-storage/12-ingestioncheck.yaml b/tests/templates/kuttl/s3-deep-storage/12-ingestioncheck.yaml index c5b501e7..f9165a35 100644 --- a/tests/templates/kuttl/s3-deep-storage/12-ingestioncheck.yaml +++ b/tests/templates/kuttl/s3-deep-storage/12-ingestioncheck.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: - - script: kubectl cp -n $NAMESPACE ./ingestioncheck.py checks-0:/tmp - - script: kubectl cp -n $NAMESPACE ./druid-quickstartimport.json checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/ingestioncheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/druid-quickstartimport.json checks-0:/tmp diff --git a/tests/templates/kuttl/s3-deep-storage/druid-quickstartimport.json b/tests/templates/kuttl/s3-deep-storage/druid-quickstartimport.json deleted file mode 100644 index 909b9008..00000000 --- a/tests/templates/kuttl/s3-deep-storage/druid-quickstartimport.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "type": "index_parallel", - "spec": { - "ioConfig": { - "type": "index_parallel", - "inputSource": { - "type": "local", - "baseDir": "quickstart/tutorial/", - "filter": "wikiticker-2015-09-12-sampled.json.gz" - }, - "inputFormat": { - "type": "json" - } - }, - "tuningConfig": { - "type": "index_parallel", - "partitionsSpec": { - "type": "dynamic" - } - }, - "dataSchema": { - "dataSource": "wikipedia-2015-09-12", - "timestampSpec": { - "column": "time", - "format": "iso" - }, - "dimensionsSpec": { - "dimensions": [ - "channel", - "cityName", - "comment", - "countryIsoCode", - "countryName", - "isAnonymous", - "isMinor", - "isNew", - "isRobot", - "isUnpatrolled", - "metroCode", - "namespace", - "page", - "regionIsoCode", - "regionName", - "user", - { - "type": "long", - "name": "delta" - }, - { - "type": "long", - "name": "added" - }, - { - "type": "long", - "name": "deleted" - } - ] - }, - "granularitySpec": { - "queryGranularity": "none", - "rollup": false, - "segmentGranularity": "day" - } - } - } -} diff --git a/tests/templates/kuttl/s3-deep-storage/ingestioncheck.py b/tests/templates/kuttl/s3-deep-storage/ingestioncheck.py deleted file mode 100755 index dd0c1b9e..00000000 --- a/tests/templates/kuttl/s3-deep-storage/ingestioncheck.py +++ /dev/null @@ -1,117 +0,0 @@ -import urllib - -import requests -import http -import sys -import json -import time - - -class DruidClient: - def __init__(self): - self.session = requests.Session() - self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) - http.client.HTTPConnection.debuglevel = 1 - - def get(self, url): - response = self.session.get(url) - assert response.status_code == 200 - return response.text - - def get_tasks(self, url): - response = self.session.get(url) - assert response.status_code == 200 - return response.text - - def post_task(self, url, input): - response = self.session.post(url, data=open(input, 'rb')) - assert response.status_code == 200 - return response.text - - def check_rc(self, url): - response = self.session.get(url) - return response.status_code - - def query_datasource(self, url, sql, expected, iterations): - loop = 0 - while True: - response = self.session.post(url, json=sql) - assert response.status_code == 200 - actual = list(json.loads(response.text)[0].values())[0] - if (actual == expected) | (loop == iterations): - break - time.sleep(5) - loop += 1 - return actual - - -druid_cluster_name = sys.argv[1] -druid = DruidClient() - -print(''' -Query tasks -===========''') -tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", -) -task_count = len(json.loads(tasks)) -print(f'existing tasks: {task_count}') - -print(''' -Start ingestion task -====================''') -ingestion = druid.post_task( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task", - input='/tmp/druid-quickstartimport.json' -) -task_id = json.loads(ingestion)["task"] -url_encoded_taskid = urllib.parse.quote(task_id, safe='') -print(f"TASKID: [{task_id}]") -print(''' -Re-query tasks -==============''') -tasks = druid.get_tasks( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/tasks", -) -new_task_count = len(json.loads(tasks)) -print(f'new tasks: {new_task_count}') -print(f'assert {new_task_count} == {task_count+1}') -assert new_task_count == task_count + 1 - -print(''' -Wait for ingestion task to succeed -======================================''') -job_finished = False -while not job_finished: - time.sleep(5) - task = druid.get( - url=f"http://{druid_cluster_name}-coordinator-default:8081/druid/indexer/v1/task/{url_encoded_taskid}/status", - ) - task_status = json.loads(task)["status"]["statusCode"] - print(f"Current task status: [{task_status}]") - assert task_status == "RUNNING" or task_status == "SUCCESS", f"Taskstatus not running or succeeeded: {task_status}" - job_finished = task_status == "SUCCESS" - -print(''' -Wait for broker to indicate all segments are fully online -======================================''') -broker_ready = False -while not broker_ready: - time.sleep(2) - broker_ready_rc = druid.check_rc(f"http://{druid_cluster_name}-broker-default:8082/druid/broker/v1/readiness") - broker_ready = broker_ready_rc == 200 - print(f"Broker respondend with [{broker_ready_rc}] to readiness check") - -print(''' -Datasource SQL -==============''') -sample_data_size = 39244 -result = druid.query_datasource( - url=f"http://{druid_cluster_name}-broker-default:8082/druid/v2/sql", - sql={"query": "select count(*) as c from \"wikipedia-2015-09-12\""}, - expected=sample_data_size, - iterations=12 -) -print(f'results: {result}') -print(f'assert {sample_data_size} == {result}') -assert sample_data_size == result diff --git a/tests/templates/kuttl/smoke/03-assert.yaml b/tests/templates/kuttl/smoke/03-assert.yaml index c1954f85..a37d231d 100644 --- a/tests/templates/kuttl/smoke/03-assert.yaml +++ b/tests/templates/kuttl/smoke/03-assert.yaml @@ -30,17 +30,29 @@ spec: template: spec: volumes: - - configMap: - name: druid-historical-default - name: config - - emptyDir: {} - name: rwconfig - - configMap: - name: druid-hdfs - name: hdfs - - emptyDir: - sizeLimit: 1G - name: segment-cache + - name: tls-mount + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: pod,node + creationTimestamp: null + spec: + storageClassName: secrets.stackable.tech + - name: tls + emptyDir: {} + - name: config + configMap: + name: druid-historical-default + - name: rwconfig + emptyDir: {} + - name: hdfs + configMap: + name: druid-hdfs + - name: segment-cache + emptyDir: + sizeLimit: 1G --- apiVersion: apps/v1 kind: StatefulSet diff --git a/tests/templates/kuttl/smoke/03-install-druid.yaml.j2 b/tests/templates/kuttl/smoke/03-install-druid.yaml.j2 index 3eee51ed..abf9c8d9 100644 --- a/tests/templates/kuttl/smoke/03-install-druid.yaml.j2 +++ b/tests/templates/kuttl/smoke/03-install-druid.yaml.j2 @@ -25,7 +25,6 @@ spec: port: 5432 user: druid password: druid - tls: null zookeeperConfigMapName: druid-znode brokers: roleGroups: diff --git a/tests/templates/kuttl/smoke/11-assert.yaml b/tests/templates/kuttl/smoke/11-assert.yaml index bf22ffe7..d0c0e99e 100644 --- a/tests/templates/kuttl/smoke/11-assert.yaml +++ b/tests/templates/kuttl/smoke/11-assert.yaml @@ -2,5 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert commands: - - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py + - script: kubectl exec -n $NAMESPACE checks-0 -- python /tmp/healthcheck.py druid timeout: 300 diff --git a/tests/templates/kuttl/smoke/11-healthcheck.yaml b/tests/templates/kuttl/smoke/11-healthcheck.yaml index 4738b350..bb53960e 100644 --- a/tests/templates/kuttl/smoke/11-healthcheck.yaml +++ b/tests/templates/kuttl/smoke/11-healthcheck.yaml @@ -3,4 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./healthcheck.py checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/healthcheck.py checks-0:/tmp diff --git a/tests/templates/kuttl/smoke/healthcheck.py b/tests/templates/kuttl/smoke/healthcheck.py deleted file mode 100755 index 8de39f42..00000000 --- a/tests/templates/kuttl/smoke/healthcheck.py +++ /dev/null @@ -1,63 +0,0 @@ -import requests -import sys -import logging -import time - -if __name__ == "__main__": - result = 0 - - log_level = 'DEBUG' # if args.debug else 'INFO' - logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s: %(message)s', stream=sys.stdout) - - druid_cluster_name = "druid" - druid_roles = [ - "broker", - "coordinator", - "middlemanager", - "historical", - "router" - ] - druid_ports = { - "broker": 8082, - "coordinator": 8081, - "middlemanager": 8091, - "historical": 8083, - "router": 8888 - } - - for role in druid_roles: - url = f"http://{druid_cluster_name}-{role}-default:{druid_ports[role]}/status/health" - count = 1 - - # As this script is intended to be executed by Kuttl which is in charge of overall test timeouts it is ok - # to loop infinitely here - or until all tests succeed - # The script iterates over all known ports and services and checks that the ports are available - # The timeout for this connection attempt is configured to 5 seconds, to ensure frequent retries that are - # not handled internally by the requests library, because it was unclear when or if dns entries are cached - # internally during retry handling. - # By issuing a new call to .get() we are trying to ensure a new dns lookup for the target. - # - # Any errors are logged and retried until either the test succeeds or Kuttl kills this script due to - # the timeout. - while True: - try: - count = count + 1 - print(f"Checking role [{role}] on url [{url}]") - res = requests.get(url, timeout=5) - code = res.status_code - if res.status_code == 200 and res.text.lower() == "true": - break - else: - print(f"Got non 200 status code [{res.status_code}] or non-true response [{res.text.lower()}], retrying attempt no [{count}] ....") - except requests.exceptions.Timeout: - print(f"Connection timed out, retrying attempt no [{count}] ....") - except requests.ConnectionError as e: - print(f"Connection Error: {str(e)}") - except requests.RequestException as e: - print(f"General Error: {str(e)}") - except Exception: - print(f"Unhandled error occurred, retrying attempt no [{count}] ....") - - # Wait a little bit before retrying - time.sleep(1) - sys.exit(0) diff --git a/tests/templates/kuttl/tls/03-assert.yaml.j2 b/tests/templates/kuttl/tls/03-assert.yaml.j2 index cb7954a4..3cc45147 100644 --- a/tests/templates/kuttl/tls/03-assert.yaml.j2 +++ b/tests/templates/kuttl/tls/03-assert.yaml.j2 @@ -53,16 +53,16 @@ spec: port: 9090 protocol: TCP targetPort: 9090 -{% if test_scenario['values']['use-tls'] == 'false' %} - - name: http - port: 8081 - protocol: TCP - targetPort: 8081 -{% else %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} - name: https port: 8281 protocol: TCP targetPort: 8281 +{% else %} + - name: http + port: 8081 + protocol: TCP + targetPort: 8081 {% endif %} --- apiVersion: v1 @@ -75,16 +75,16 @@ spec: port: 9090 protocol: TCP targetPort: 9090 -{% if test_scenario['values']['use-tls'] == 'false' %} - - name: http - port: 8091 - protocol: TCP - targetPort: 8091 -{% else %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} - name: https port: 8291 protocol: TCP targetPort: 8291 +{% else %} + - name: http + port: 8091 + protocol: TCP + targetPort: 8091 {% endif %} --- apiVersion: v1 @@ -97,16 +97,16 @@ spec: port: 9090 protocol: TCP targetPort: 9090 -{% if test_scenario['values']['use-tls'] == 'false' %} - - name: http - port: 8083 - protocol: TCP - targetPort: 8083 -{% else %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} - name: https port: 8283 protocol: TCP targetPort: 8283 +{% else %} + - name: http + port: 8083 + protocol: TCP + targetPort: 8083 {% endif %} --- apiVersion: v1 @@ -119,16 +119,16 @@ spec: port: 9090 protocol: TCP targetPort: 9090 -{% if test_scenario['values']['use-tls'] == 'false' %} - - name: http - port: 8888 - protocol: TCP - targetPort: 8888 -{% else %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} - name: https port: 9088 protocol: TCP targetPort: 9088 +{% else %} + - name: http + port: 8888 + protocol: TCP + targetPort: 8888 {% endif %} --- apiVersion: v1 @@ -141,14 +141,14 @@ spec: port: 9090 protocol: TCP targetPort: 9090 -{% if test_scenario['values']['use-tls'] == 'false' %} - - name: http - port: 8082 - protocol: TCP - targetPort: 8082 -{% else %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} - name: https port: 8282 protocol: TCP targetPort: 8282 +{% else %} + - name: http + port: 8082 + protocol: TCP + targetPort: 8082 {% endif %} diff --git a/tests/templates/kuttl/tls/03-install-druid.yaml.j2 b/tests/templates/kuttl/tls/03-install-druid.yaml.j2 index 45b7945a..90ec1491 100644 --- a/tests/templates/kuttl/tls/03-install-druid.yaml.j2 +++ b/tests/templates/kuttl/tls/03-install-druid.yaml.j2 @@ -24,18 +24,17 @@ metadata: stringData: accessKey: druid secretKey: druiddruid -{% if test_scenario['values']['use-tls'] == 'true' and test_scenario['values']['use-tls-auth'] == 'true' %} --- apiVersion: secrets.stackable.tech/v1alpha1 kind: SecretClass metadata: - name: druid-tls-auth + name: druid-tls spec: backend: autoTls: ca: secret: - name: secret-provisioner-druid-tls-auth-ca + name: secret-provisioner-druid-tls-ca namespace: default autoGenerate: true --- @@ -46,8 +45,7 @@ metadata: spec: provider: tls: - clientCertSecretClass: druid-tls-auth -{% endif %} + clientCertSecretClass: druid-tls # This SecretClass must match the SecretClass used for internal Druid communication --- apiVersion: druid.stackable.tech/v1alpha1 kind: DruidCluster @@ -58,10 +56,9 @@ spec: productVersion: "{{ test_scenario['values']['druid-latest'].split('-stackable')[0] }}" stackableVersion: "{{ test_scenario['values']['druid-latest'].split('-stackable')[1] }}" clusterConfig: -{% if test_scenario['values']['use-tls'] == 'true' and test_scenario['values']['use-tls-auth'] == 'true' %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} authentication: - tls: - authenticationClass: druid-tls-auth-class + - authenticationClass: druid-tls-auth-class {% endif %} deepStorage: s3: @@ -86,9 +83,9 @@ spec: connString: jdbc:derby://localhost:1527/var/druid/metadata.db;create=true host: localhost port: 1527 -{% if test_scenario['values']['use-tls'] == 'true' %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} tls: - secretClass: tls + serverAndInternalSecretClass: druid-tls {% else %} tls: null {% endif %} diff --git a/tests/templates/kuttl/tls/04-install-checks.yaml.j2 b/tests/templates/kuttl/tls/04-install-checks.yaml.j2 index 76fefe76..b089c790 100644 --- a/tests/templates/kuttl/tls/04-install-checks.yaml.j2 +++ b/tests/templates/kuttl/tls/04-install-checks.yaml.j2 @@ -23,45 +23,38 @@ spec: image: docker.stackable.tech/stackable/testing-tools:0.1.0-stackable0.1.0 command: ["sleep", "infinity"] volumeMounts: - - mountPath: /tmp/tls - name: tls -{% if test_scenario['values']['use-tls'] == 'true' and test_scenario['values']['use-tls-auth'] == 'true' %} - - mountPath: /tmp/tls_auth - name: tls-auth -{% endif %} +{% if test_scenario['values']['tls-mode'] == 'internal-and-server-tls' or test_scenario['values']['tls-mode'] == 'internal-and-server-tls-and-tls-client-auth' %} + - name: druid-tls + mountPath: /tmp/druid-tls + - name: tls + mountPath: /tmp/tls volumes: - - ephemeral: + - name: druid-tls + ephemeral: volumeClaimTemplate: metadata: annotations: - secrets.stackable.tech/class: tls + secrets.stackable.tech/class: druid-tls secrets.stackable.tech/scope: pod,node - creationTimestamp: null spec: + storageClassName: secrets.stackable.tech accessModes: - ReadWriteOnce resources: requests: storage: "1" - storageClassName: secrets.stackable.tech - volumeMode: Filesystem - name: tls -{% if test_scenario['values']['use-tls'] == 'true' and test_scenario['values']['use-tls-auth'] == 'true' %} - - ephemeral: + - name: tls + ephemeral: volumeClaimTemplate: metadata: annotations: - secrets.stackable.tech/class: druid-tls-auth + secrets.stackable.tech/class: tls secrets.stackable.tech/scope: pod,node - creationTimestamp: null spec: + storageClassName: secrets.stackable.tech accessModes: - ReadWriteOnce resources: requests: storage: "1" - storageClassName: secrets.stackable.tech - volumeMode: Filesystem - name: tls-auth {% endif %} - diff --git a/tests/templates/kuttl/tls/10-assert.yaml.j2 b/tests/templates/kuttl/tls/10-assert.yaml.j2 index db6032ef..d2bb2146 100644 --- a/tests/templates/kuttl/tls/10-assert.yaml.j2 +++ b/tests/templates/kuttl/tls/10-assert.yaml.j2 @@ -3,12 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 300 commands: -{% if test_scenario['values']['use-tls-auth'] == 'true' and test_scenario['values']['use-tls'] == 'true' %} - - script: kubectl exec -n $NAMESPACE druid-checks-0 -- /tmp/check-tls.sh $NAMESPACE secure_auth -{% endif %} -{% if test_scenario['values']['use-tls-auth'] == 'false' and test_scenario['values']['use-tls'] == 'true' %} - - script: kubectl exec -n $NAMESPACE druid-checks-0 -- /tmp/check-tls.sh $NAMESPACE secure -{% endif %} -{% if test_scenario['values']['use-tls-auth'] == 'false' and test_scenario['values']['use-tls'] == 'false' %} - - script: kubectl exec -n $NAMESPACE druid-checks-0 -- /tmp/check-tls.sh $NAMESPACE insecure -{% endif %} + - script: kubectl exec -n $NAMESPACE druid-checks-0 -- /tmp/check-tls.sh $NAMESPACE {{ test_scenario['values']['tls-mode'] }} diff --git a/tests/templates/kuttl/tls/10-tls-checks.yaml b/tests/templates/kuttl/tls/10-tls-checks.yaml index f48a57d8..9ff950a9 100644 --- a/tests/templates/kuttl/tls/10-tls-checks.yaml +++ b/tests/templates/kuttl/tls/10-tls-checks.yaml @@ -4,4 +4,4 @@ kind: TestStep timeout: 600 commands: - script: kubectl cp -n $NAMESPACE ./check-tls.sh druid-checks-0:/tmp/check-tls.sh - - script: kubectl cp -n $NAMESPACE ./untrusted-ca.crt druid-checks-0:/tmp/tls/untrusted-ca.crt + - script: kubectl cp -n $NAMESPACE ./untrusted-ca.crt druid-checks-0:/tmp/untrusted-ca.crt diff --git a/tests/templates/kuttl/tls/11-assert.yaml.j2 b/tests/templates/kuttl/tls/11-assert.yaml.j2 index 8f02e3ac..4c3e8c3a 100644 --- a/tests/templates/kuttl/tls/11-assert.yaml.j2 +++ b/tests/templates/kuttl/tls/11-assert.yaml.j2 @@ -3,12 +3,4 @@ apiVersion: kuttl.dev/v1beta1 kind: TestAssert timeout: 300 commands: -{% if test_scenario['values']['use-tls-auth'] == 'true' and test_scenario['values']['use-tls'] == 'true' %} - - script: kubectl exec -n $NAMESPACE druid-checks-0 -- python /tmp/ingestioncheck.py $NAMESPACE derby-druid secure_auth -{% endif %} -{% if test_scenario['values']['use-tls-auth'] == 'false' and test_scenario['values']['use-tls'] == 'true' %} - - script: kubectl exec -n $NAMESPACE druid-checks-0 -- python /tmp/ingestioncheck.py $NAMESPACE derby-druid secure -{% endif %} -{% if test_scenario['values']['use-tls-auth'] == 'false' and test_scenario['values']['use-tls'] == 'false' %} - - script: kubectl exec -n $NAMESPACE druid-checks-0 -- python /tmp/ingestioncheck.py $NAMESPACE derby-druid insecure -{% endif %} + - script: kubectl exec -n $NAMESPACE druid-checks-0 -- python /tmp/ingestioncheck-tls.py $NAMESPACE derby-druid {{ test_scenario['values']['tls-mode'] }} diff --git a/tests/templates/kuttl/tls/11-ingestion-checks.yaml b/tests/templates/kuttl/tls/11-ingestion-checks.yaml index d13a3f78..a72b5461 100644 --- a/tests/templates/kuttl/tls/11-ingestion-checks.yaml +++ b/tests/templates/kuttl/tls/11-ingestion-checks.yaml @@ -3,5 +3,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep timeout: 600 commands: - - script: kubectl cp -n $NAMESPACE ./ingestioncheck.py druid-checks-0:/tmp - - script: kubectl cp -n $NAMESPACE ./druid-quickstartimport.json druid-checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/ingestioncheck-tls.py druid-checks-0:/tmp + - script: kubectl cp -n $NAMESPACE ../../../../templates/kuttl/commons/druid-quickstartimport.json druid-checks-0:/tmp diff --git a/tests/templates/kuttl/tls/check-tls.sh b/tests/templates/kuttl/tls/check-tls.sh index 416d5c8c..69682d6d 100755 --- a/tests/templates/kuttl/tls/check-tls.sh +++ b/tests/templates/kuttl/tls/check-tls.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# Usage: check-tls.sh namespace type [insecure,secure,secure_auth] +# Usage: check-tls.sh namespace type [no-tls,internal-and-server-tls,internal-and-server-tls-and-tls-client-auth] NAMESPACE=$1 TYPE=$2 # No encryption -if [[ $TYPE == "insecure" ]] +if [[ $TYPE == "no-tls" ]] then HOST=http://derby-druid-router-default-0.derby-druid-router-default.${NAMESPACE}.svc.cluster.local:8888/status/health @@ -21,7 +21,7 @@ then fi # Only encryption -if [[ $TYPE == "secure" ]] +if [[ $TYPE == "internal-and-server-tls" ]] then HOST=https://derby-druid-router-default-0.derby-druid-router-default.${NAMESPACE}.svc.cluster.local:9088/status/health @@ -47,7 +47,7 @@ then # should work without insecure but with certificate echo "[TLS_ENCRYPTION] Test TLS with trusted certificate" - if curl --cacert /tmp/tls/ca.crt "$HOST" &> /dev/null + if curl --cacert /tmp/druid-tls/ca.crt "$HOST" &> /dev/null then echo "[SUCCESS] Could establish connection to server with trusted certificate!" else @@ -57,7 +57,7 @@ then # should not work with wrong certificate echo "[TLS_ENCRYPTION] Test TLS with untrusted certificate" - if curl --cacert /tmp/tls/untrusted-ca.crt "$HOST" &> /dev/null + if curl --cacert /tmp/untrusted-ca.crt "$HOST" &> /dev/null then echo "[ERROR] Could establish connection to server with untrusted certificate. Should not be happening!" exit 1 @@ -67,7 +67,7 @@ then fi # Encryption and TLS client auth -if [[ $TYPE == "secure_auth" ]] +if [[ $TYPE == "internal-and-server-tls-and-tls-client-auth" ]] then HOST=https://derby-druid-router-default-0.derby-druid-router-default.${NAMESPACE}.svc.cluster.local:9088/status/health @@ -83,7 +83,7 @@ then # Should fail echo "[TLS_AUTH] Test access providing CA" - if curl --cacert "$HOST" &> /dev/null + if curl --cacert /tmp/druid-tls/ca.crt "$HOST" &> /dev/null then echo "[ERROR] Could establish insecure connection to server! This should not be happening!" exit 1 @@ -92,8 +92,8 @@ then fi # Should fail - echo "[TLS_AUTH] Test access providing wrong ca, cert and key" - if curl --cacert /tmp/tls/ca.crt --cert /tmp/tls/tls.crt --key /tmp/tls/tls.key "$HOST" &> /dev/null + echo "[TLS_AUTH] Test access providing wrong cert and key" + if curl --cacert /tmp/druid-tls/ca.crt --cert /tmp/tls/tls.crt --key /tmp/tls/tls.key "$HOST" &> /dev/null then echo "[ERROR] Could establish authenticated connection to server with wrong credentials! This should not be happening!" exit 1 @@ -103,7 +103,7 @@ then # Should work echo "[TLS_AUTH] Test access providing correct ca, cert and key" - if curl --cacert /tmp/tls_auth/ca.crt --cert /tmp/tls_auth/tls.crt --key /tmp/tls_auth/tls.key "$HOST" &> /dev/null + if curl --cacert /tmp/druid-tls/ca.crt --cert /tmp/druid-tls/tls.crt --key /tmp/druid-tls/tls.key "$HOST" &> /dev/null then echo "[SUCCESS] Could establish authenticated connection to server!" else diff --git a/tests/templates/kuttl/tls/druid-quickstartimport.json b/tests/templates/kuttl/tls/druid-quickstartimport.json deleted file mode 100644 index 909b9008..00000000 --- a/tests/templates/kuttl/tls/druid-quickstartimport.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "type": "index_parallel", - "spec": { - "ioConfig": { - "type": "index_parallel", - "inputSource": { - "type": "local", - "baseDir": "quickstart/tutorial/", - "filter": "wikiticker-2015-09-12-sampled.json.gz" - }, - "inputFormat": { - "type": "json" - } - }, - "tuningConfig": { - "type": "index_parallel", - "partitionsSpec": { - "type": "dynamic" - } - }, - "dataSchema": { - "dataSource": "wikipedia-2015-09-12", - "timestampSpec": { - "column": "time", - "format": "iso" - }, - "dimensionsSpec": { - "dimensions": [ - "channel", - "cityName", - "comment", - "countryIsoCode", - "countryName", - "isAnonymous", - "isMinor", - "isNew", - "isRobot", - "isUnpatrolled", - "metroCode", - "namespace", - "page", - "regionIsoCode", - "regionName", - "user", - { - "type": "long", - "name": "delta" - }, - { - "type": "long", - "name": "added" - }, - { - "type": "long", - "name": "deleted" - } - ] - }, - "granularitySpec": { - "queryGranularity": "none", - "rollup": false, - "segmentGranularity": "day" - } - } - } -} diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index e005b357..faf084b9 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -24,14 +24,11 @@ dimensions: values: - "true" - "false" - - name: use-tls + - name: tls-mode values: - - "true" - - "false" - - name: use-tls-auth - values: - - "true" - - "false" + - "no-tls" + - "internal-and-server-tls" + - "internal-and-server-tls-and-tls-client-auth" tests: - name: smoke dimensions: @@ -77,5 +74,4 @@ tests: dimensions: - druid-latest - zookeeper-latest - - use-tls - - use-tls-auth + - tls-mode