-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathdiscovery.rs
More file actions
288 lines (263 loc) · 10.4 KB
/
Copy pathdiscovery.rs
File metadata and controls
288 lines (263 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
use std::{collections::BTreeSet, num::TryFromIntError, str::FromStr};
use snafu::{OptionExt, ResultExt, Snafu};
use stackable_operator::{
builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder},
crd::listener,
k8s_openapi::api::core::v1::ConfigMap,
kube::{Resource, runtime::reflector::ObjectRef},
v2::{
HasName, HasUid, NameIsValidLabelValue,
builder::meta::ownerreference_from_resource,
kvp::label::recommended_labels,
types::operator::{ControllerName, ProductVersion},
},
};
use crate::{
crd::{ZOOKEEPER_SERVER_PORT_NAME, ZookeeperRole, security::ZookeeperSecurity},
zk_controller::{
build::PLACEHOLDER_DISCOVERY_ROLE_GROUP,
validate::{ValidatedCluster, operator_name, product_name},
},
znode_controller::validate::ValidatedZnode,
};
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("chroot path {} was relative (must be absolute)", chroot))]
RelativeChroot { chroot: String },
#[snafu(display("{listener} does not have a port with the name {port_name:?}"))]
PortNotFound {
port_name: String,
listener: ObjectRef<listener::v1alpha1::Listener>,
},
#[snafu(display("expected an unsigned 16-bit port, got {port_number}"))]
InvalidPort {
source: TryFromIntError,
port_number: i32,
},
#[snafu(display("{listener} has no ingress addresses"))]
NoListenerIngressAddresses {
listener: ObjectRef<listener::v1alpha1::Listener>,
},
#[snafu(display("failed to build ConfigMap"))]
BuildConfigMap {
source: stackable_operator::builder::configmap::Error,
},
}
/// Build the discovery [`ConfigMap`] for the cluster controller from the
/// [`ValidatedCluster`].
///
/// The ConfigMap is owned by, and placed in the namespace of, the cluster. The image and security
/// settings are taken from the [`ValidatedCluster`] rather than being passed in separately.
pub fn build_discovery_configmap(
validated_cluster: &ValidatedCluster,
controller_name: &str,
listener: listener::v1alpha1::Listener,
) -> Result<ConfigMap> {
build_discovery_configmap_for_owner(
validated_cluster,
&validated_cluster.namespace,
controller_name,
&validated_cluster.product_version,
listener,
None,
&validated_cluster.cluster_config.zookeeper_security,
)
}
/// Build the discovery [`ConfigMap`] for the znode controller.
///
/// The ConfigMap is owned by, and placed in the namespace of, the
/// [`ValidatedZnode`]. The product version and `zookeeper_security` originate from the referenced
/// cluster (via the validated znode), while `chroot` isolates the znode within the shared ZooKeeper
/// ensemble.
pub fn build_znode_discovery_configmap(
validated_znode: &ValidatedZnode,
controller_name: &str,
listener: listener::v1alpha1::Listener,
chroot: &str,
) -> Result<ConfigMap> {
build_discovery_configmap_for_owner(
validated_znode,
&validated_znode.namespace,
controller_name,
&validated_znode.product_version,
listener,
Some(chroot),
&validated_znode.zookeeper_security,
)
}
/// Build a discovery [`ConfigMap`] containing ZooKeeper connection details from a
/// [`listener::v1alpha1::Listener`].
///
/// `owner` owns the ConfigMap (the [`ZookeeperCluster`](crate::crd::v1alpha1::ZookeeperCluster) for the cluster
/// controller, or the [`ZookeeperZnode`](crate::crd::v1alpha1::ZookeeperZnode) for the znode controller) and
/// `namespace` is where the ConfigMap is placed.
fn build_discovery_configmap_for_owner(
owner: &(impl Resource<DynamicType = ()> + HasName + HasUid + NameIsValidLabelValue),
namespace: impl Into<String>,
controller_name: &str,
product_version: &ProductVersion,
listener: listener::v1alpha1::Listener,
chroot: Option<&str>,
zookeeper_security: &ZookeeperSecurity,
) -> Result<ConfigMap> {
let name = owner.to_name();
// The discovery ConfigMap is a role-level resource of the `server` role, conventionally
// labelled with the `discovery` role group. The controller name differs between the cluster and
// znode controllers, so it is passed in and validated into the type-safe newtype here.
let controller_name = ControllerName::from_str(controller_name)
.expect("the controller name is a valid label value");
let role_group_name = PLACEHOLDER_DISCOVERY_ROLE_GROUP.clone();
let listener_addresses = listener_addresses(&listener, ZOOKEEPER_SERVER_PORT_NAME)?;
// Write a connection string of the format that Java ZooKeeper client expects:
// "{host1}:{port1},{host2:port2},.../{chroot}"
// See https://zookeeper.apache.org/doc/current/apidocs/zookeeper-server/org/apache/zookeeper/ZooKeeper.html#ZooKeeper-java.lang.String-int-org.apache.zookeeper.Watcher-
let listener_addresses = listener_addresses
.into_iter()
.map(|(host, port)| format!("{host}:{port}"))
.collect::<Vec<_>>()
.join(",");
let mut conn_str = listener_addresses.clone();
if let Some(chroot) = chroot {
if !chroot.starts_with('/') {
return RelativeChrootSnafu { chroot }.fail();
}
conn_str.push_str(chroot);
}
ConfigMapBuilder::new()
.metadata(
ObjectMetaBuilder::new()
.name(name)
.namespace(namespace)
.ownerreference(ownerreference_from_resource(owner, None, Some(true)))
.with_labels(recommended_labels(
owner,
&product_name(),
product_version,
&operator_name(),
&controller_name,
&ZookeeperRole::Server.into(),
&role_group_name,
))
.build(),
)
.add_data("ZOOKEEPER", conn_str)
// Some clients don't support ZooKeeper's merged `hosts/chroot` format, so export them separately for these clients
.add_data("ZOOKEEPER_HOSTS", listener_addresses)
.add_data(
"ZOOKEEPER_CLIENT_PORT",
zookeeper_security.client_port().to_string(),
)
.add_data("ZOOKEEPER_CHROOT", chroot.unwrap_or("/"))
.build()
.context(BuildConfigMapSnafu)
}
/// Lists all listener address and port number pairs for a given `port_name` for Pods participating in the [`Listener`][1]
///
/// This returns pairs of `(Address, Port)`, where address could be a hostname or IP address of a node, clusterIP or external
/// load balancer depending on the Service type.
///
/// ## Errors
///
/// An error will be returned if there is no address found for the `port_name`.
///
/// [1]: listener::v1alpha1::Listener
// TODO (@NickLarsenNZ): Move this to stackable-operator, so it can be used as listener.addresses_for_port(port_name)
fn listener_addresses(
listener: &listener::v1alpha1::Listener,
port_name: &str,
) -> Result<impl IntoIterator<Item = (String, u16)> + use<>> {
// Get addresses port pairs for addresses that have a port with the name that matches the one we are interested in
let address_port_pairs = listener
.status
.as_ref()
.and_then(|listener_status| listener_status.ingress_addresses.as_ref())
.context(NoListenerIngressAddressesSnafu { listener })?
.iter()
// Filter the addresses that have the port we are interested in (they likely all have it though)
.filter_map(|listener_ingress| {
Some(listener_ingress.address.clone()).zip(listener_ingress.ports.get(port_name))
})
// Convert the port from i32 to u16
.map(|(listener_address, &port_number)| {
let port_number: u16 = port_number
.try_into()
.context(InvalidPortSnafu { port_number })?;
Ok((listener_address, port_number))
})
.collect::<Result<BTreeSet<_>, _>>()?;
// An empty list is considered an error
match address_port_pairs.is_empty() {
true => PortNotFoundSnafu {
port_name,
listener,
}
.fail(),
false => Ok(address_port_pairs),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use stackable_operator::{
crd::listener::v1alpha1::{
AddressType, Listener, ListenerIngress, ListenerSpec, ListenerStatus,
},
k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta,
};
use super::*;
fn listener(ingress_addresses: Option<Vec<ListenerIngress>>) -> Listener {
Listener {
metadata: ObjectMeta {
name: Some("test-listener".to_owned()),
..ObjectMeta::default()
},
spec: ListenerSpec::default(),
status: Some(ListenerStatus {
service_name: None,
ingress_addresses,
node_ports: None,
}),
}
}
fn ingress(port: i32) -> ListenerIngress {
ListenerIngress {
address: "node-0".to_owned(),
address_type: AddressType::Hostname,
ports: BTreeMap::from([(ZOOKEEPER_SERVER_PORT_NAME.to_owned(), port)]),
}
}
#[test]
fn listener_addresses_returns_host_port_pairs() {
let listener = listener(Some(vec![ingress(2181)]));
let pairs: Vec<_> = listener_addresses(&listener, ZOOKEEPER_SERVER_PORT_NAME)
.expect("addresses")
.into_iter()
.collect();
assert_eq!(pairs, vec![("node-0".to_owned(), 2181u16)]);
}
#[test]
fn listener_addresses_without_ingress_is_error() {
assert!(matches!(
listener_addresses(&listener(None), ZOOKEEPER_SERVER_PORT_NAME),
Err(Error::NoListenerIngressAddresses { .. })
));
}
#[test]
fn listener_addresses_missing_port_name_is_error() {
let listener = listener(Some(vec![ingress(2181)]));
assert!(matches!(
listener_addresses(&listener, "does-not-exist"),
Err(Error::PortNotFound { .. })
));
}
#[test]
fn listener_addresses_port_out_of_u16_range_is_error() {
// A port number that does not fit into a u16 must be rejected.
let listener = listener(Some(vec![ingress(70_000)]));
assert!(matches!(
listener_addresses(&listener, ZOOKEEPER_SERVER_PORT_NAME),
Err(Error::InvalidPort { .. })
));
}
}