diff --git a/CHANGELOG.md b/CHANGELOG.md index 540535da..09734b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ All notable changes to this project will be documented in this file. ### Added - More CRD documentation ([#433]). +- Support for exposing HDFS clusters to clients outside of Kubernetes ([#450]). ### Changed - `operator-rs` `0.56.1` -> `0.57.0` ([#433]). +- [BREAKING] `.spec.clusterConfig.listenerClass` has been renamed to `.spec.nameNodes.config.listenerClass`, migration will be required when using `external-unstable` ([#450]). - Change default value of `dfs.ha.nn.not-become-active-in-safemode` from `true` to `false` ([#458]). ### Fixed @@ -19,6 +21,7 @@ All notable changes to this project will be documented in this file. and `dfs.datanode.kerberos.principal` in the discovery ConfigMap in case Kerberos is enabled ([#451]). [#433]: https://github.com/stackabletech/hdfs-operator/pull/433 +[#450]: https://github.com/stackabletech/hdfs-operator/pull/450 [#451]: https://github.com/stackabletech/hdfs-operator/pull/451 [#458]: https://github.com/stackabletech/hdfs-operator/pull/458 diff --git a/Cargo.lock b/Cargo.lock index 8acab649..0ac9350f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1947,6 +1947,7 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" name = "stackable-hdfs-crd" version = "0.0.0-dev" dependencies = [ + "futures 0.3.28", "product-config", "rstest", "semver", diff --git a/deploy/helm/hdfs-operator/crds/crds.yaml b/deploy/helm/hdfs-operator/crds/crds.yaml index 4c6e54ac..4809a6e8 100644 --- a/deploy/helm/hdfs-operator/crds/crds.yaml +++ b/deploy/helm/hdfs-operator/crds/crds.yaml @@ -57,20 +57,6 @@ spec: format: uint8 minimum: 0.0 type: integer - listenerClass: - default: cluster-internal - description: |- - This field controls which type of Service the Operator creates for this HdfsCluster: - - * cluster-internal: Use a ClusterIP service - - * external-unstable: Use a NodePort service - - This is a temporary solution with the goal to keep yaml manifests forward compatible. In the future, this setting will control which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) will be used to expose the service, and ListenerClass names will stay the same, allowing for a non-breaking change. - enum: - - cluster-internal - - external-unstable - type: string vectorAggregatorConfigMapName: description: Name of the Vector aggregator [discovery ConfigMap](https://docs.stackable.tech/home/nightly/concepts/service_discovery). It must contain the key `ADDRESS` with the address of the Vector aggregator. Follow the [logging tutorial](https://docs.stackable.tech/home/nightly/tutorials/logging-vector-aggregator) to learn how to configure log aggregation with Vector. nullable: true @@ -585,6 +571,10 @@ spec: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. nullable: true type: string + listenerClass: + description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose this rolegroup. DataNodes should have a direct ListenerClass, such as `cluster-internal` or `external-unstable`. + nullable: true + type: string logging: default: enableVectorAgent: null @@ -4093,6 +4083,10 @@ spec: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. nullable: true type: string + listenerClass: + description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose this rolegroup. DataNodes should have a direct ListenerClass, such as `cluster-internal` or `external-unstable`. + nullable: true + type: string logging: default: enableVectorAgent: null @@ -14669,6 +14663,10 @@ spec: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. nullable: true type: string + listenerClass: + description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose this rolegroup. NameNodes should have a stable ListenerClass, such as `cluster-internal` or `external-stable`. + nullable: true + type: string logging: default: enableVectorAgent: null @@ -18168,6 +18166,10 @@ spec: description: Time period Pods have to gracefully shut down, e.g. `30m`, `1h` or `2d`. Consult the operator documentation for details. nullable: true type: string + listenerClass: + description: This field controls which [ListenerClass](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass.html) is used to expose this rolegroup. NameNodes should have a stable ListenerClass, such as `cluster-internal` or `external-stable`. + nullable: true + type: string logging: default: enableVectorAgent: null diff --git a/deploy/helm/hdfs-operator/templates/roles.yaml b/deploy/helm/hdfs-operator/templates/roles.yaml index 148fc323..252bf8c4 100644 --- a/deploy/helm/hdfs-operator/templates/roles.yaml +++ b/deploy/helm/hdfs-operator/templates/roles.yaml @@ -88,6 +88,12 @@ rules: - events verbs: - create + - apiGroups: + - listeners.stackable.tech + resources: + - listeners + verbs: + - get - apiGroups: - {{ include "operator.name" . }}.stackable.tech resources: diff --git a/docs/modules/hdfs/examples/getting_started/hdfs.yaml b/docs/modules/hdfs/examples/getting_started/hdfs.yaml index 511793c4..b307e212 100644 --- a/docs/modules/hdfs/examples/getting_started/hdfs.yaml +++ b/docs/modules/hdfs/examples/getting_started/hdfs.yaml @@ -5,16 +5,19 @@ metadata: name: simple-hdfs spec: image: - productVersion: 3.3.6 + productVersion: 3.3.4 clusterConfig: zookeeperConfigMapName: simple-hdfs-znode - listenerClass: external-unstable dfsReplication: 1 nameNodes: + config: + listenerClass: external-stable roleGroups: default: replicas: 2 dataNodes: + config: + listenerClass: external-unstable roleGroups: default: replicas: 1 diff --git a/docs/modules/hdfs/pages/usage-guide/listenerclass.adoc b/docs/modules/hdfs/pages/usage-guide/listenerclass.adoc index 05437804..06a2213f 100644 --- a/docs/modules/hdfs/pages/usage-guide/listenerclass.adoc +++ b/docs/modules/hdfs/pages/usage-guide/listenerclass.adoc @@ -1,15 +1,18 @@ = Service exposition with ListenerClasses -The Operator deploys a service called `--` (where `` is the name of the HdfsCluster, `` is the role and `` the name of the role group) through which the different HDFS processes can be accessed. Unlike many other Stackable Operators, the HDFS Operator does not deploy role-level Services. +The operator deploys a xref:listener-operator:listener.adoc[Listener] for each DataNode and NameNode pod. They both default to only being accessible from within the Kubernetes cluster, but this can be changed by setting `.spec.{data,name}Nodes.config.listenerClass`. -These services can have either the `cluster-internal` or `external-unstable` type. `external-stable` is not supported for HDFS at the moment. Read more about the types in the xref:concepts:service-exposition.adoc[service exposition] documentation at platform level. - -This is how the listener class is configured: +The cluster can be configured to be accessible from outside of Kubernetes like this: [source,yaml] ---- spec: - clusterConfig: - listenerClass: cluster-internal # <1> + dataNodes: + config: + listenerClass: external-unstable # <1> + nameNodes: + config: + listenerClass: external-stable # <2> ---- -<1> The default `cluster-internal` setting. +<1> DataNode listeners should prioritize having a direct connection, to minimize network transfer overhead. +<2> NameNode listeners should prioritize having a stable address, since they will be baked into the client configuration. diff --git a/rust/crd/Cargo.toml b/rust/crd/Cargo.toml index b2e1ac37..618ff59e 100644 --- a/rust/crd/Cargo.toml +++ b/rust/crd/Cargo.toml @@ -17,6 +17,7 @@ stackable-operator.workspace = true product-config.workspace = true strum.workspace = true tracing.workspace = true +futures.workspace = true [dev-dependencies] serde_yaml.workspace = true diff --git a/rust/crd/src/constants.rs b/rust/crd/src/constants.rs index 5fe554b6..d1fda8bd 100644 --- a/rust/crd/src/constants.rs +++ b/rust/crd/src/constants.rs @@ -9,9 +9,6 @@ pub const FIELD_MANAGER_SCOPE_POD: &str = "pod-service"; pub const APP_NAME: &str = "hdfs"; -pub const LABEL_ENABLE: &str = "hdfs.stackable.tech/pod-service"; -pub const LABEL_STS_POD_NAME: &str = "statefulset.kubernetes.io/pod-name"; - pub const HDFS_SITE_XML: &str = "hdfs-site.xml"; pub const CORE_SITE_XML: &str = "core-site.xml"; pub const HADOOP_POLICY_XML: &str = "hadoop-policy.xml"; @@ -27,6 +24,8 @@ pub const SERVICE_PORT_NAME_HTTPS: &str = "https"; pub const SERVICE_PORT_NAME_DATA: &str = "data"; pub const SERVICE_PORT_NAME_METRICS: &str = "metrics"; +pub const DEFAULT_LISTENER_CLASS: &str = "cluster-internal"; + pub const DEFAULT_NAME_NODE_METRICS_PORT: u16 = 8183; pub const DEFAULT_NAME_NODE_HTTP_PORT: u16 = 9870; pub const DEFAULT_NAME_NODE_HTTPS_PORT: u16 = 9871; @@ -77,4 +76,7 @@ pub const JOURNALNODE_ROOT_DATA_DIR: &str = "/stackable/data/journalnode"; pub const DATANODE_ROOT_DATA_DIR_PREFIX: &str = "/stackable/data/"; pub const DATANODE_ROOT_DATA_DIR_SUFFIX: &str = "/datanode"; +pub const LISTENER_VOLUME_NAME: &str = "listener"; +pub const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; + pub const HDFS_UID: i64 = 1000; diff --git a/rust/crd/src/lib.rs b/rust/crd/src/lib.rs index 39364a54..26ebd9ab 100644 --- a/rust/crd/src/lib.rs +++ b/rust/crd/src/lib.rs @@ -2,9 +2,11 @@ use std::{ borrow::Cow, collections::{BTreeMap, HashMap}, fmt::Display, + num::TryFromIntError, ops::Deref, }; +use futures::future::try_join_all; use product_config::types::PropertyNameKind; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; @@ -12,6 +14,7 @@ use stackable_operator::{ commons::{ affinity::StackableAffinity, cluster_operation::ClusterOperation, + listener::Listener, product_image_selection::ProductImage, resources::{ CpuLimitsFragment, MemoryLimitsFragment, NoRuntimeLimits, NoRuntimeLimitsFragment, @@ -24,6 +27,7 @@ use stackable_operator::{ merge::Merge, }, k8s_openapi::{ + api::core::v1::Pod, api::core::v1::PodTemplateSpec, apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::LabelSelector}, }, @@ -49,6 +53,9 @@ use crate::{ }, }; +#[cfg(doc)] +use stackable_operator::commons::listener::ListenerClass; + pub mod affinity; pub mod constants; pub mod security; @@ -56,14 +63,31 @@ pub mod storage; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("Object has no associated namespace"))] + #[snafu(display("object has no associated namespace"))] NoNamespace, - #[snafu(display("Missing node role [{role}]"))] + #[snafu(display("missing node role {role:?}"))] MissingRole { role: String }, - #[snafu(display("Missing role group [{role_group}] for role [{role}]"))] + #[snafu(display("missing role group {role_group:?} for role {role:?}"))] MissingRoleGroup { role: String, role_group: String }, #[snafu(display("fragment validation failure"))] FragmentValidationFailure { source: ValidationError }, + #[snafu(display("unable to get {listener} (for {pod})"))] + GetPodListener { + source: stackable_operator::error::Error, + listener: ObjectRef, + pod: ObjectRef, + }, + #[snafu(display("{listener} (for {pod}) has no address"))] + PodListenerHasNoAddress { + listener: ObjectRef, + pod: ObjectRef, + }, + #[snafu(display("port {port} ({port_name:?}) is out of bounds, must be within {range:?}", range = 0..=u16::MAX))] + PortOutOfBounds { + source: TryFromIntError, + port_name: String, + port: i32, + }, } /// An HDFS cluster stacklet. This resource is managed by the Stackable operator for Apache Hadoop HDFS. @@ -132,18 +156,6 @@ pub struct HdfsClusterConfig { /// for a ZooKeeper cluster. pub zookeeper_config_map_name: String, - /// This field controls which type of Service the Operator creates for this HdfsCluster: - /// - /// * cluster-internal: Use a ClusterIP service - /// - /// * external-unstable: Use a NodePort service - /// - /// This is a temporary solution with the goal to keep yaml manifests forward compatible. - /// In the future, this setting will control which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) - /// will be used to expose the service, and ListenerClass names will stay the same, allowing for a non-breaking change. - #[serde(default)] - pub listener_class: CurrentlySupportedListenerClasses, - /// Settings related to user [authentication](DOCS_BASE_URL_PLACEHOLDER/usage-guide/security). pub authentication: Option, } @@ -152,28 +164,6 @@ fn default_dfs_replication_factor() -> u8 { DEFAULT_DFS_REPLICATION_FACTOR } -// TODO: Temporary solution until listener-operator is finished -#[derive( - Clone, Debug, Default, Display, Deserialize, Eq, Hash, JsonSchema, PartialEq, Serialize, -)] -#[serde(rename_all = "PascalCase")] -pub enum CurrentlySupportedListenerClasses { - #[default] - #[serde(rename = "cluster-internal")] - ClusterInternal, - #[serde(rename = "external-unstable")] - ExternalUnstable, -} - -impl CurrentlySupportedListenerClasses { - pub fn k8s_service_type(&self) -> String { - match self { - CurrentlySupportedListenerClasses::ClusterInternal => "ClusterIP".to_string(), - CurrentlySupportedListenerClasses::ExternalUnstable => "NodePort".to_string(), - } - } -} - /// Configuration options that are available for all roles. #[derive(Clone, Debug, Default, Fragment, JsonSchema, PartialEq)] #[fragment_attrs( @@ -531,15 +521,6 @@ impl HdfsCluster { group_labels.insert(String::from("role"), rolegroup_ref.role.clone()); group_labels.insert(String::from("group"), rolegroup_ref.role_group.clone()); - if self.spec.cluster_config.listener_class - == CurrentlySupportedListenerClasses::ExternalUnstable - { - // TODO: in a production environment, probably not all roles need to be exposed with one NodePort per Pod but it's - // useful for development purposes. - - group_labels.insert(LABEL_ENABLE.to_string(), "true".to_string()); - } - group_labels } @@ -629,9 +610,13 @@ impl HdfsCluster { } } - /// List all [HdfsPodRef]s expected for the given `role` + /// List all [`HdfsPodRef`]s expected for the given [`role`](HdfsRole). /// /// The `validated_config` is used to extract the ports exposed by the pods. + /// + /// The pod refs returned by `pod_refs` will only be able to able to access HDFS + /// from inside the Kubernetes cluster. For configuring downstream clients, + /// consider using [`Self::namenode_listener_refs`] instead. pub fn pod_refs(&self, role: &HdfsRole) -> Result, Error> { let ns = self.metadata.namespace.clone().context(NoNamespaceSnafu)?; @@ -650,11 +635,66 @@ impl HdfsCluster { .iter() .map(|(n, p)| (n.clone(), *p)) .collect(), + fqdn_override: None, }) }) .collect()) } + /// List all [`HdfsPodRef`]s for the running namenodes, configured to access the cluster via + /// [Listener] rather than direct [Pod] access. + /// + /// This enables access from outside the Kubernetes cluster (if using a [ListenerClass] configured for this). + /// + /// This method assumes that all [Listener]s have been created, and may fail while waiting for the cluster to come online. + /// If this is unacceptable (mainly for configuring the cluster itself), consider [`Self::pod_refs`] instead. + /// + /// This method _only_ supports accessing namenodes, since journalnodes are considered internal, and datanodes are registered + /// dynamically with the namenodes. + pub async fn namenode_listener_refs( + &self, + client: &stackable_operator::client::Client, + ) -> Result, Error> { + let pod_refs = self.pod_refs(&HdfsRole::NameNode)?; + try_join_all(pod_refs.into_iter().map(|pod_ref| async { + let listener_name = format!("{LISTENER_VOLUME_NAME}-{}", pod_ref.pod_name); + let listener_ref = + || ObjectRef::::new(&listener_name).within(&pod_ref.namespace); + let pod_obj_ref = + || ObjectRef::::new(&pod_ref.pod_name).within(&pod_ref.namespace); + let listener = client + .get::(&listener_name, &pod_ref.namespace) + .await + .context(GetPodListenerSnafu { + listener: listener_ref(), + pod: pod_obj_ref(), + })?; + let listener_address = listener + .status + .and_then(|s| s.ingress_addresses?.into_iter().next()) + .context(PodListenerHasNoAddressSnafu { + listener: listener_ref(), + pod: pod_obj_ref(), + })?; + Ok(HdfsPodRef { + fqdn_override: Some(listener_address.address), + ports: listener_address + .ports + .into_iter() + .map(|(port_name, port)| { + let port = u16::try_from(port).context(PortOutOfBoundsSnafu { + port_name: &port_name, + port, + })?; + Ok((port_name, port)) + }) + .collect::>()?, + ..pod_ref + }) + })) + .await + } + pub fn rolegroup_ref_and_replicas( &self, role: &HdfsRole, @@ -884,14 +924,20 @@ pub struct HdfsPodRef { pub namespace: String, pub role_group_service_name: String, pub pod_name: String, + pub fqdn_override: Option, pub ports: HashMap, } impl HdfsPodRef { - pub fn fqdn(&self) -> String { - format!( - "{}.{}.{}.svc.cluster.local", - self.pod_name, self.role_group_service_name, self.namespace + pub fn fqdn(&self) -> Cow { + self.fqdn_override.as_deref().map_or_else( + || { + Cow::Owned(format!( + "{}.{}.{}.svc.cluster.local", + self.pod_name, self.role_group_service_name, self.namespace + )) + }, + Cow::Borrowed, ) } } @@ -942,6 +988,10 @@ pub struct NameNodeConfig { pub resources: Resources, #[fragment_attrs(serde(default))] pub logging: Logging, + /// This field controls which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) is used to expose this rolegroup. + /// NameNodes should have a stable ListenerClass, such as `cluster-internal` or `external-stable`. + #[fragment_attrs(serde(default))] + pub listener_class: String, #[fragment_attrs(serde(flatten))] pub common: CommonNodeConfig, } @@ -967,6 +1017,7 @@ impl NameNodeConfigFragment { }, }, logging: product_logging::spec::default_logging(), + listener_class: Some(DEFAULT_LISTENER_CLASS.to_string()), common: CommonNodeConfigFragment { affinity: get_affinity(cluster_name, role), graceful_shutdown_timeout: Some(DEFAULT_NAME_NODE_GRACEFUL_SHUTDOWN_TIMEOUT), @@ -1054,6 +1105,10 @@ pub struct DataNodeConfig { pub resources: Resources, #[fragment_attrs(serde(default))] pub logging: Logging, + /// This field controls which [ListenerClass](DOCS_BASE_URL_PLACEHOLDER/listener-operator/listenerclass.html) is used to expose this rolegroup. + /// DataNodes should have a direct ListenerClass, such as `cluster-internal` or `external-unstable`. + #[fragment_attrs(serde(default))] + pub listener_class: String, #[fragment_attrs(serde(flatten))] pub common: CommonNodeConfig, } @@ -1084,6 +1139,7 @@ impl DataNodeConfigFragment { )]), }, logging: product_logging::spec::default_logging(), + listener_class: Some(DEFAULT_LISTENER_CLASS.to_string()), common: CommonNodeConfigFragment { affinity: get_affinity(cluster_name, role), graceful_shutdown_timeout: Some(DEFAULT_DATA_NODE_GRACEFUL_SHUTDOWN_TIMEOUT), diff --git a/rust/operator-binary/src/config.rs b/rust/operator-binary/src/config.rs index 331b4d41..9c49ef27 100644 --- a/rust/operator-binary/src/config.rs +++ b/rust/operator-binary/src/config.rs @@ -5,7 +5,8 @@ use stackable_hdfs_crd::constants::{ DFS_JOURNALNODE_RPC_ADDRESS, DFS_NAMENODE_HTTPS_ADDRESS, DFS_NAMENODE_HTTP_ADDRESS, DFS_NAMENODE_NAME_DIR, DFS_NAMENODE_RPC_ADDRESS, DFS_NAMENODE_SHARED_EDITS_DIR, DFS_NAME_SERVICES, DFS_REPLICATION, FS_DEFAULT_FS, HA_ZOOKEEPER_QUORUM, - JOURNALNODE_ROOT_DATA_DIR, NAMENODE_ROOT_DATA_DIR, + JOURNALNODE_ROOT_DATA_DIR, NAMENODE_ROOT_DATA_DIR, SERVICE_PORT_NAME_HTTP, + SERVICE_PORT_NAME_HTTPS, SERVICE_PORT_NAME_RPC, }; use stackable_hdfs_crd::storage::{DataNodeStorageConfig, DataNodeStorageConfigInnerType}; use stackable_hdfs_crd::{HdfsCluster, HdfsPodRef}; @@ -141,6 +142,7 @@ impl HdfsSiteConfigBuilder { self.dfs_namenode_address_ha( namenode_podrefs, DFS_NAMENODE_RPC_ADDRESS, + SERVICE_PORT_NAME_RPC, DEFAULT_NAME_NODE_RPC_PORT, ); self @@ -155,12 +157,14 @@ impl HdfsSiteConfigBuilder { self.dfs_namenode_address_ha( namenode_podrefs, DFS_NAMENODE_HTTPS_ADDRESS, + SERVICE_PORT_NAME_HTTPS, DEFAULT_NAME_NODE_HTTPS_PORT, ); } else { self.dfs_namenode_address_ha( namenode_podrefs, DFS_NAMENODE_HTTP_ADDRESS, + SERVICE_PORT_NAME_HTTP, DEFAULT_NAME_NODE_HTTP_PORT, ); } @@ -171,6 +175,7 @@ impl HdfsSiteConfigBuilder { &mut self, namenode_podrefs: &[HdfsPodRef], address: &str, + port_name: &str, default_port: u16, ) -> &mut Self { for nn in namenode_podrefs { @@ -179,7 +184,7 @@ impl HdfsSiteConfigBuilder { format!( "{}:{}", nn.fqdn(), - nn.ports.get(address).map_or(default_port, |p| *p) + nn.ports.get(port_name).map_or(default_port, |p| *p) ), ); } diff --git a/rust/operator-binary/src/container.rs b/rust/operator-binary/src/container.rs index e59c5e0a..26a906fa 100644 --- a/rust/operator-binary/src/container.rs +++ b/rust/operator-binary/src/container.rs @@ -22,20 +22,14 @@ use stackable_hdfs_crd::{ constants::{ DATANODE_ROOT_DATA_DIR_PREFIX, DEFAULT_DATA_NODE_METRICS_PORT, DEFAULT_JOURNAL_NODE_METRICS_PORT, DEFAULT_NAME_NODE_METRICS_PORT, - JVM_SECURITY_PROPERTIES_FILE, LOG4J_PROPERTIES, NAMENODE_ROOT_DATA_DIR, - SERVICE_PORT_NAME_IPC, SERVICE_PORT_NAME_RPC, STACKABLE_ROOT_DATA_DIR, + JVM_SECURITY_PROPERTIES_FILE, LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, LOG4J_PROPERTIES, + NAMENODE_ROOT_DATA_DIR, SERVICE_PORT_NAME_IPC, SERVICE_PORT_NAME_RPC, + STACKABLE_ROOT_DATA_DIR, }, storage::DataNodeStorageConfig, AnyNodeConfig, DataNodeContainer, HdfsCluster, HdfsPodRef, HdfsRole, LoggingExt as _, NameNodeContainer, }; -use stackable_operator::{ - builder::SecretFormat, - product_logging::framework::{ - create_vector_shutdown_file_command, remove_vector_shutdown_file_command, - }, - utils::COMMON_BASH_TRAP_FUNCTIONS, -}; use stackable_operator::{ builder::{ resources::ResourceRequirementsBuilder, ContainerBuilder, PodBuilder, @@ -50,7 +44,7 @@ use stackable_operator::{ }, apimachinery::pkg::util::intstr::IntOrString, }, - kube::ResourceExt, + kube::{core::ObjectMeta, ResourceExt}, memory::{BinaryMultiple, MemoryQuantity}, product_logging::{ self, @@ -60,6 +54,13 @@ use stackable_operator::{ }, }, }; +use stackable_operator::{ + builder::{ListenerOperatorVolumeSourceBuilder, ListenerReference, SecretFormat}, + product_logging::framework::{ + create_vector_shutdown_file_command, remove_vector_shutdown_file_command, + }, + utils::COMMON_BASH_TRAP_FUNCTIONS, +}; use std::{collections::BTreeMap, str::FromStr}; use strum::{Display, EnumDiscriminants, IntoStaticStr}; @@ -291,10 +292,28 @@ impl ContainerConfig { pub fn volume_claim_templates(merged_config: &AnyNodeConfig) -> Vec { match merged_config { - AnyNodeConfig::NameNode(node) => vec![node.resources.storage.data.build_pvc( - ContainerConfig::DATA_VOLUME_MOUNT_NAME, - Some(vec!["ReadWriteOnce"]), - )], + AnyNodeConfig::NameNode(node) => { + let listener = ListenerOperatorVolumeSourceBuilder::new( + &ListenerReference::ListenerClass(node.listener_class.to_string()), + ) + .build() + .volume_claim_template + .unwrap(); + vec![ + node.resources.storage.data.build_pvc( + ContainerConfig::DATA_VOLUME_MOUNT_NAME, + Some(vec!["ReadWriteOnce"]), + ), + PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some(LISTENER_VOLUME_NAME.to_string()), + ..listener.metadata.unwrap() + }, + spec: Some(listener.spec), + ..Default::default() + }, + ] + } AnyNodeConfig::JournalNode(node) => vec![node.resources.storage.data.build_pvc( ContainerConfig::DATA_VOLUME_MOUNT_NAME, Some(vec!["ReadWriteOnce"]), @@ -442,14 +461,20 @@ impl ContainerConfig { )); args.push_str(&format!( - "\ + r#"\ {COMMON_BASH_TRAP_FUNCTIONS} {remove_vector_shutdown_file_command} prepare_signal_handlers +if [[ -d {LISTENER_VOLUME_DIR} ]]; then + export POD_ADDRESS=$(cat {LISTENER_VOLUME_DIR}/default-address/address) + for i in {LISTENER_VOLUME_DIR}/default-address/ports/*; do + export $(basename $i | tr a-z A-Z)_PORT="$(cat $i)" + done +fi {hadoop_home}/bin/hdfs {role} & wait_for_termination $! {create_vector_shutdown_file_command} -", +"#, hadoop_home = Self::HADOOP_HOME, remove_vector_shutdown_file_command = remove_vector_shutdown_file_command(STACKABLE_LOG_DIR), @@ -763,27 +788,40 @@ wait_for_termination $! fn volumes(&self, merged_config: &AnyNodeConfig, object_name: &str) -> Vec { let mut volumes = vec![]; - let container_log_config = match self { - ContainerConfig::Hdfs { .. } => { + if let ContainerConfig::Hdfs { .. } = self { + if let AnyNodeConfig::DataNode(node) = merged_config { volumes.push( - VolumeBuilder::new(ContainerConfig::STACKABLE_LOG_VOLUME_MOUNT_NAME) - .empty_dir(EmptyDirVolumeSource { - medium: None, - size_limit: Some( - product_logging::framework::calculate_log_volume_size_limit(&[ - MAX_HDFS_LOG_FILE_SIZE, - MAX_ZKFC_LOG_FILE_SIZE, - MAX_FORMAT_NAMENODE_LOG_FILE_SIZE, - MAX_FORMAT_ZOOKEEPER_LOG_FILE_SIZE, - MAX_WAIT_NAMENODES_LOG_FILE_SIZE, - ]), - ), - }) + VolumeBuilder::new(LISTENER_VOLUME_NAME) + .ephemeral( + ListenerOperatorVolumeSourceBuilder::new( + &ListenerReference::ListenerClass(node.listener_class.to_string()), + ) + .build(), + ) .build(), ); - - Some(merged_config.hdfs_logging()) } + + volumes.push( + VolumeBuilder::new(ContainerConfig::STACKABLE_LOG_VOLUME_MOUNT_NAME) + .empty_dir(EmptyDirVolumeSource { + medium: None, + size_limit: Some( + product_logging::framework::calculate_log_volume_size_limit(&[ + MAX_HDFS_LOG_FILE_SIZE, + MAX_ZKFC_LOG_FILE_SIZE, + MAX_FORMAT_NAMENODE_LOG_FILE_SIZE, + MAX_FORMAT_ZOOKEEPER_LOG_FILE_SIZE, + MAX_WAIT_NAMENODES_LOG_FILE_SIZE, + ]), + ), + }) + .build(), + ); + } + + let container_log_config = match self { + ContainerConfig::Hdfs { .. } => Some(merged_config.hdfs_logging()), ContainerConfig::Zkfc { .. } => merged_config .as_namenode() .map(|node| node.logging.for_container(&NameNodeContainer::Zkfc)), @@ -800,7 +838,6 @@ wait_for_termination $! .for_container(&DataNodeContainer::WaitForNameNodes) }), }; - volumes.extend(Self::common_container_volumes( container_log_config.as_deref(), object_name, @@ -846,27 +883,37 @@ wait_for_termination $! .build(), ); } - ContainerConfig::Hdfs { role, .. } => match role { - HdfsRole::NameNode | HdfsRole::JournalNode => { + ContainerConfig::Hdfs { role, .. } => { + // JournalNode doesn't use listeners, since it's only used internally by the namenodes + if let HdfsRole::NameNode | HdfsRole::DataNode = role { volume_mounts.push( - VolumeMountBuilder::new( - Self::DATA_VOLUME_MOUNT_NAME, - STACKABLE_ROOT_DATA_DIR, - ) - .build(), + VolumeMountBuilder::new(LISTENER_VOLUME_NAME, LISTENER_VOLUME_DIR).build(), ); } - HdfsRole::DataNode => { - for pvc in Self::volume_claim_templates(merged_config) { - let pvc_name = pvc.name_any(); - volume_mounts.push(VolumeMount { - mount_path: format!("{DATANODE_ROOT_DATA_DIR_PREFIX}{pvc_name}"), - name: pvc_name, - ..VolumeMount::default() - }); + + // Add data volume + match role { + HdfsRole::NameNode | HdfsRole::JournalNode => { + volume_mounts.push( + VolumeMountBuilder::new( + Self::DATA_VOLUME_MOUNT_NAME, + STACKABLE_ROOT_DATA_DIR, + ) + .build(), + ); + } + HdfsRole::DataNode => { + for pvc in Self::volume_claim_templates(merged_config) { + let pvc_name = pvc.name_any(); + volume_mounts.push(VolumeMount { + mount_path: format!("{DATANODE_ROOT_DATA_DIR_PREFIX}{pvc_name}"), + name: pvc_name, + ..VolumeMount::default() + }); + } } } - }, + } // The other containers don't need any data pvcs to be mounted ContainerConfig::Zkfc { .. } | ContainerConfig::WaitForNameNodes { .. } diff --git a/rust/operator-binary/src/hdfs_controller.rs b/rust/operator-binary/src/hdfs_controller.rs index 61cf7cb8..e40cbfc0 100644 --- a/rust/operator-binary/src/hdfs_controller.rs +++ b/rust/operator-binary/src/hdfs_controller.rs @@ -128,7 +128,10 @@ pub enum Error { role_group: String, }, - #[snafu(display("Cannot build config discovery config map"))] + #[snafu(display("Cannot collect discovery configuration"))] + CollectDiscoveryConfig { source: stackable_hdfs_crd::Error }, + + #[snafu(display("Cannot build discovery config map"))] BuildDiscoveryConfigMap { source: stackable_operator::error::Error, }, @@ -272,22 +275,6 @@ pub async fn reconcile_hdfs(hdfs: Arc, ctx: Arc) -> HdfsOperat ) .context(CreateClusterResourcesSnafu)?; - let discovery_cm = build_discovery_configmap( - &hdfs, - HDFS_CONTROLLER, - &namenode_podrefs, - &resolved_product_image, - )?; - - // The discovery CM is linked to the cluster lifecycle via ownerreference. - // Therefore, must not be added to the "orphaned" cluster resources - client - .apply_patch(FIELD_MANAGER_SCOPE, &discovery_cm, &discovery_cm) - .await - .with_context(|_| ApplyDiscoveryConfigMapSnafu { - name: discovery_cm.metadata.name.clone().unwrap_or_default(), - })?; - // The service account and rolebinding will be created per cluster let (rbac_sa, rbac_rolebinding) = build_rbac_resources( hdfs.as_ref(), @@ -393,6 +380,27 @@ pub async fn reconcile_hdfs(hdfs: Arc, ctx: Arc) -> HdfsOperat } } + // Discovery CM will fail to build until the rest of the cluster has been deployed, so do it last + // so that failure won't inhibit the rest of the cluster from booting up. + let discovery_cm = build_discovery_configmap( + &hdfs, + HDFS_CONTROLLER, + &hdfs + .namenode_listener_refs(client) + .await + .context(CollectDiscoveryConfigSnafu)?, + &resolved_product_image, + )?; + + // The discovery CM is linked to the cluster lifecycle via ownerreference. + // Therefore, must not be added to the "orphaned" cluster resources + client + .apply_patch(FIELD_MANAGER_SCOPE, &discovery_cm, &discovery_cm) + .await + .with_context(|_| ApplyDiscoveryConfigMapSnafu { + name: discovery_cm.metadata.name.clone().unwrap_or_default(), + })?; + let cluster_operation_cond_builder = ClusterOperationsConditionBuilder::new(&hdfs.spec.cluster_operation); @@ -503,7 +511,8 @@ fn rolegroup_config_map( // This caused a deadlock with no namenode becoming active during a startup after // HDFS was completely down for a while. - hdfs_site_xml = HdfsSiteConfigBuilder::new(hdfs_name.to_string()) + let mut builder = HdfsSiteConfigBuilder::new(hdfs_name.to_string()); + builder .dfs_namenode_name_dir() .dfs_datanode_data_dir( merged_config @@ -523,9 +532,20 @@ fn rolegroup_config_map( .add("dfs.ha.fencing.methods", "shell(/bin/true)") .add("dfs.ha.automatic-failover.enabled", "true") .add("dfs.ha.namenode.id", "${env.POD_NAME}") - // the extend with config must come last in order to have overrides working!!! - .extend(config) - .build_as_xml(); + .add( + "dfs.namenode.datanode.registration.unsafe.allow-address-override", + "true", + ) + .add("dfs.datanode.registered.hostname", "${env.POD_ADDRESS}") + .add("dfs.datanode.registered.port", "${env.DATA_PORT}") + .add("dfs.datanode.registered.ipc.port", "${env.IPC_PORT}"); + if hdfs.has_https_enabled() { + builder.add("dfs.datanode.registered.https.port", "${env.HTTPS_PORT}"); + } else { + builder.add("dfs.datanode.registered.http.port", "${env.HTTP_PORT}"); + } + // the extend with config must come last in order to have overrides working!!! + hdfs_site_xml = builder.extend(config).build_as_xml(); } PropertyNameKind::File(file_name) if file_name == CORE_SITE_XML => { core_site_xml = CoreSiteConfigBuilder::new(hdfs_name.to_string()) diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index ac060338..27e49671 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -9,7 +9,7 @@ use stackable_operator::{ client::{self, Client}, k8s_openapi::api::{ apps::v1::StatefulSet, - core::v1::{ConfigMap, Pod, Service}, + core::v1::{ConfigMap, Service}, }, kube::runtime::{watcher, Controller}, labels::ObjectLabels, @@ -27,7 +27,6 @@ mod event; mod hdfs_controller; mod kerberos; mod operations; -mod pod_svc_controller; mod product_logging; mod built_info { @@ -114,28 +113,7 @@ pub async fn create_controller( .map(|res| report_controller_reconciled(&client, CONTROLLER_NAME, &res)) .instrument(info_span!("hdfs_controller")); - let pod_svc_controller = Controller::new( - namespace.get_api::(&client), - watcher::Config::default().labels(&format!("{}=true", LABEL_ENABLE)), - ) - .owns( - namespace.get_api::(&client), - watcher::Config::default(), - ) - .shutdown_on_signal() - .run( - pod_svc_controller::reconcile_pod, - pod_svc_controller::error_policy, - Arc::new(pod_svc_controller::Ctx { - client: client.clone(), - }), - ) - .map(|res| report_controller_reconciled(&client, &format!("pod-svc.{OPERATOR_NAME}"), &res)) - .instrument(info_span!("pod_svc_controller")); - - futures::stream::select(hdfs_controller, pod_svc_controller) - .collect::<()>() - .await; + hdfs_controller.collect::<()>().await; } /// Creates recommended `ObjectLabels` to be used in deployed resources diff --git a/rust/operator-binary/src/pod_svc_controller.rs b/rust/operator-binary/src/pod_svc_controller.rs deleted file mode 100644 index dc9af76f..00000000 --- a/rust/operator-binary/src/pod_svc_controller.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! NodePort controller for exposing individual Pods. -//! -//! For pods with the label `hdfs.stackable.tech/pod-service=true` a NodePort is created that exposes the local node pod. -use snafu::{OptionExt, ResultExt, Snafu}; -use stackable_hdfs_crd::constants::*; -use stackable_hdfs_crd::HdfsRole; -use stackable_operator::{ - builder::ObjectMetaBuilder, - k8s_openapi::api::core::v1::{Pod, Service, ServicePort, ServiceSpec}, - kube::runtime::controller::Action, - logging::controller::ReconcilerError, - time::Duration, -}; -use std::sync::Arc; -use strum::{EnumDiscriminants, IntoStaticStr}; - -#[derive(Snafu, Debug, EnumDiscriminants)] -#[strum_discriminants(derive(IntoStaticStr))] -pub enum Error { - #[snafu(display("Pod has no name"))] - PodHasNoName, - #[snafu(display("Pod [{name}] has no labels"))] - PodHasNoLabels { name: String }, - #[snafu(display("Pod [{name}] has no spec"))] - PodHasNoSpec { name: String }, - #[snafu(display("Failed to build owner reference of pod [{name}]"))] - PodOwnerReference { - source: stackable_operator::error::Error, - name: String, - }, - #[snafu(display("Cannot create pod service [{name}]"))] - ApplyPodServiceFailed { - source: stackable_operator::error::Error, - name: String, - }, -} - -impl ReconcilerError for Error { - fn category(&self) -> &'static str { - ErrorDiscriminants::from(self).into() - } -} - -pub struct Ctx { - pub client: stackable_operator::client::Client, -} - -const APP_KUBERNETES_LABEL_BASE: &str = "app.kubernetes.io/"; - -pub async fn reconcile_pod(pod: Arc, ctx: Arc) -> Result { - tracing::info!("Starting reconcile"); - - let name = pod.metadata.name.clone().context(PodHasNoNameSnafu)?; - - let pod_labels = pod - .metadata - .labels - .as_ref() - .with_context(|| PodHasNoLabelsSnafu { name: name.clone() })?; - - let recommended_labels_from_pod = pod_labels - .iter() - .filter(|(key, _)| key.starts_with(APP_KUBERNETES_LABEL_BASE)) - .map(|(key, value)| (key.clone(), value.clone())) - .collect(); - - let ports: Vec<(String, i32)> = pod - .spec - .as_ref() - .with_context(|| PodHasNoSpecSnafu { name: name.clone() })? - .containers - .iter() - .filter(|container| { - container.name == HdfsRole::NameNode.to_string() - || container.name == HdfsRole::DataNode.to_string() - || container.name == HdfsRole::JournalNode.to_string() - }) - .flat_map(|c| c.ports.as_ref()) - .flat_map(|cp| cp.iter()) - .map(|cp| (cp.name.clone().unwrap_or_default(), cp.container_port)) - .collect(); - - let svc = Service { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(pod.as_ref()) - .labels(recommended_labels_from_pod) - .ownerreference_from_resource(pod.as_ref(), None, None) - .with_context(|_| PodOwnerReferenceSnafu { name: name.clone() })? - .build(), - spec: Some(ServiceSpec { - type_: Some("NodePort".to_string()), - external_traffic_policy: Some("Local".to_string()), - ports: Some( - ports - .iter() - .map(|(name, port)| ServicePort { - name: Some(name.clone()), - port: *port, - ..ServicePort::default() - }) - .collect(), - ), - selector: Some([(LABEL_STS_POD_NAME.to_string(), name.clone())].into()), - publish_not_ready_addresses: Some(true), - ..ServiceSpec::default() - }), - ..Service::default() - }; - - // The pod service is deleted when the corresponding pod is deleted. - // Therefore no cluster / orphaned resources have to be handled here. - ctx.client - .apply_patch(FIELD_MANAGER_SCOPE_POD, &svc, &svc) - .await - .with_context(|_| ApplyPodServiceFailedSnafu { name })?; - - Ok(Action::await_change()) -} - -pub fn error_policy(_obj: Arc, _error: &Error, _ctx: Arc) -> Action { - Action::requeue(*Duration::from_secs(5)) -} diff --git a/tests/templates/kuttl/external-access/10-assert.yaml.j2 b/tests/templates/kuttl/external-access/10-assert.yaml.j2 new file mode 100644 index 00000000..50b1d4c3 --- /dev/null +++ b/tests/templates/kuttl/external-access/10-assert.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +{% endif %} diff --git a/tests/templates/kuttl/external-access/10-install-vector-aggregator-discovery-configmap.yaml.j2 b/tests/templates/kuttl/external-access/10-install-vector-aggregator-discovery-configmap.yaml.j2 new file mode 100644 index 00000000..2d6a0df5 --- /dev/null +++ b/tests/templates/kuttl/external-access/10-install-vector-aggregator-discovery-configmap.yaml.j2 @@ -0,0 +1,9 @@ +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +data: + ADDRESS: {{ lookup('env', 'VECTOR_AGGREGATOR') }} +{% endif %} diff --git a/tests/templates/kuttl/external-access/20-assert.yaml b/tests/templates/kuttl/external-access/20-assert.yaml new file mode 100644 index 00000000..2f8f7173 --- /dev/null +++ b/tests/templates/kuttl/external-access/20-assert.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: hdfs-zk-server-default +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/external-access/20-install-zk.yaml.j2 b/tests/templates/kuttl/external-access/20-install-zk.yaml.j2 new file mode 100644 index 00000000..5d489a4d --- /dev/null +++ b/tests/templates/kuttl/external-access/20-install-zk.yaml.j2 @@ -0,0 +1,20 @@ +--- +apiVersion: zookeeper.stackable.tech/v1alpha1 +kind: ZookeeperCluster +metadata: + name: hdfs-zk +spec: + image: + productVersion: "{{ test_scenario['values']['zookeeper-latest'] }}" + pullPolicy: IfNotPresent + clusterConfig: +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + servers: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 1 diff --git a/tests/templates/kuttl/external-access/30-assert.yaml.j2 b/tests/templates/kuttl/external-access/30-assert.yaml.j2 new file mode 100644 index 00000000..619a15ed --- /dev/null +++ b/tests/templates/kuttl/external-access/30-assert.yaml.j2 @@ -0,0 +1,28 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: hdfs-namenode-default +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: hdfs-journalnode-default +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: hdfs-datanode-default +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/external-access/30-install-hdfs.yaml.j2 b/tests/templates/kuttl/external-access/30-install-hdfs.yaml.j2 new file mode 100644 index 00000000..c1af5f95 --- /dev/null +++ b/tests/templates/kuttl/external-access/30-install-hdfs.yaml.j2 @@ -0,0 +1,44 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: install-hdfs +timeout: 180 +--- +apiVersion: hdfs.stackable.tech/v1alpha1 +kind: HdfsCluster +metadata: + name: hdfs +spec: + image: + productVersion: "{{ test_scenario['values']['hadoop-latest'] }}" + pullPolicy: IfNotPresent + clusterConfig: + zookeeperConfigMapName: hdfs-zk + dfsReplication: 1 +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + nameNodes: + config: + listenerClass: external-stable + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 2 + dataNodes: + config: + listenerClass: external-unstable + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 1 + journalNodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 1 diff --git a/tests/templates/kuttl/external-access/40-assert.yaml b/tests/templates/kuttl/external-access/40-assert.yaml new file mode 100644 index 00000000..607c4815 --- /dev/null +++ b/tests/templates/kuttl/external-access/40-assert.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + # Won't work due to https://github.com/kudobuilder/kuttl/issues/322, the `script` runs in the + # test suite dir, not the test dir. + # We _could_ move some of the preparation to a TestStep, but running a persistent Docker container would cause + # resource leakage for failed/interrupted tests. + - script: ./operate-on-cluster.sh diff --git a/tests/templates/kuttl/external-access/operate-on-cluster.sh b/tests/templates/kuttl/external-access/operate-on-cluster.sh new file mode 100755 index 00000000..6e90a6f8 --- /dev/null +++ b/tests/templates/kuttl/external-access/operate-on-cluster.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +mkdir -p etc +kubectl -n "$NAMESPACE" get cm/hdfs -o jsonpath='{.data.core-site\.xml}' > etc/core-site.xml +kubectl -n "$NAMESPACE" get cm/hdfs -o jsonpath='{.data.hdfs-site\.xml}' > etc/hdfs-site.xml + +# Run a "vanilla"/upstream container outside of k8s to ensure that the client doesn't rely on any stackableisms or running inside of k8s. +docker run --rm -it --volume "$(pwd)/etc:/opt/hadoop/etc:ro" --volume "$(pwd):/data:ro" apache/hadoop:"{{ test_scenario['values']['hadoop-external-client-docker-image'] }}" hdfs dfs -put /data/testdata.txt / diff --git a/tests/templates/kuttl/external-access/testdata.txt b/tests/templates/kuttl/external-access/testdata.txt new file mode 100755 index 00000000..d63c3679 --- /dev/null +++ b/tests/templates/kuttl/external-access/testdata.txt @@ -0,0 +1,3 @@ +What is Lorem Ipsum? + +Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 65683cbf..a6aaa1ab 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -3,8 +3,11 @@ dimensions: - name: hadoop values: - 3.2.4 - - 3.3.6 + - 3.3.4 - name: hadoop-latest + values: + - 3.3.4 + - name: hadoop-external-client-docker-image values: - 3.3.6 - name: zookeeper @@ -63,6 +66,12 @@ tests: dimensions: - hadoop-latest - zookeeper-latest + # Broken due to https://github.com/kudobuilder/kuttl/issues/322, see 40-assert.yaml for more details + # - name: external-access + # dimensions: + # - hadoop-latest # We only support external access for HDFS >= 3.3.x. + # - hadoop-external-client-docker-image + # - zookeeper-latest suites: - name: nightly patch: