Skip to content

Commit e2ac187

Browse files
committed
add headless and metrics services
1 parent 8df689b commit e2ac187

2 files changed

Lines changed: 122 additions & 10 deletions

File tree

rust/operator-binary/src/kafka_controller.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use crate::{
4949
resource::{
5050
configmap::build_rolegroup_config_map,
5151
listener::build_broker_rolegroup_bootstrap_listener,
52-
service::build_rolegroup_service,
52+
service::{build_rolegroup_headless_service, build_rolegroup_metrics_service},
5353
statefulset::{build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset},
5454
},
5555
};
@@ -347,8 +347,16 @@ pub async fn reconcile_kafka(
347347
.merged_config(kafka, &rolegroup_ref.role_group)
348348
.context(FailedToResolveConfigSnafu)?;
349349

350-
let rg_service =
351-
build_rolegroup_service(kafka, &resolved_product_image, &rolegroup_ref)
350+
let rg_headless_service = build_rolegroup_headless_service(
351+
kafka,
352+
&resolved_product_image,
353+
&rolegroup_ref,
354+
&kafka_security,
355+
)
356+
.context(BuildServiceSnafu)?;
357+
358+
let rg_metrics_service =
359+
build_rolegroup_metrics_service(kafka, &resolved_product_image, &rolegroup_ref)
352360
.context(BuildServiceSnafu)?;
353361

354362
let rg_configmap = build_rolegroup_config_map(
@@ -407,7 +415,13 @@ pub async fn reconcile_kafka(
407415
}
408416

409417
cluster_resources
410-
.add(client, rg_service)
418+
.add(client, rg_headless_service)
419+
.await
420+
.with_context(|_| ApplyRoleGroupServiceSnafu {
421+
rolegroup: rolegroup_ref.clone(),
422+
})?;
423+
cluster_resources
424+
.add(client, rg_metrics_service)
411425
.await
412426
.with_context(|_| ApplyRoleGroupServiceSnafu {
413427
rolegroup: rolegroup_ref.clone(),

rust/operator-binary/src/resource/service.rs

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ use snafu::{ResultExt, Snafu};
22
use stackable_operator::{
33
builder::meta::ObjectMetaBuilder,
44
commons::product_image_selection::ResolvedProductImage,
5-
k8s_openapi::api::core::v1::{Service, ServiceSpec},
6-
kvp::{Label, Labels},
5+
k8s_openapi::api::core::v1::{Service, ServicePort, ServiceSpec},
6+
kvp::{Annotations, Labels},
77
role_utils::RoleGroupRef,
88
};
99

1010
use crate::{
11-
crd::{APP_NAME, v1alpha1},
11+
crd::{APP_NAME, METRICS_PORT, METRICS_PORT_NAME, security::KafkaTlsSecurity, v1alpha1},
1212
kafka_controller::KAFKA_CONTROLLER_NAME,
1313
utils::build_recommended_labels,
1414
};
@@ -34,15 +34,16 @@ pub enum Error {
3434
/// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup
3535
///
3636
/// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing.
37-
pub fn build_rolegroup_service(
37+
pub fn build_rolegroup_headless_service(
3838
kafka: &v1alpha1::KafkaCluster,
3939
resolved_product_image: &ResolvedProductImage,
4040
rolegroup: &RoleGroupRef<v1alpha1::KafkaCluster>,
41+
kafka_security: &KafkaTlsSecurity,
4142
) -> Result<Service, Error> {
4243
Ok(Service {
4344
metadata: ObjectMetaBuilder::new()
4445
.name_and_namespace(kafka)
45-
.name(rolegroup.object_name())
46+
.name(rolegroup_headless_service_name(rolegroup))
4647
.ownerreference_from_resource(kafka, None, Some(true))
4748
.context(ObjectMissingMetadataForOwnerRefSnafu)?
4849
.with_recommended_labels(build_recommended_labels(
@@ -53,10 +54,10 @@ pub fn build_rolegroup_service(
5354
&rolegroup.role_group,
5455
))
5556
.context(MetadataBuildSnafu)?
56-
.with_label(Label::try_from(("prometheus.io/scrape", "true")).context(LabelBuildSnafu)?)
5757
.build(),
5858
spec: Some(ServiceSpec {
5959
cluster_ip: Some("None".to_string()),
60+
ports: Some(headless_ports(kafka_security)),
6061
selector: Some(
6162
Labels::role_group_selector(
6263
kafka,
@@ -73,3 +74,100 @@ pub fn build_rolegroup_service(
7374
status: None,
7475
})
7576
}
77+
78+
/// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label
79+
pub fn build_rolegroup_metrics_service(
80+
kafka: &v1alpha1::KafkaCluster,
81+
resolved_product_image: &ResolvedProductImage,
82+
rolegroup: &RoleGroupRef<v1alpha1::KafkaCluster>,
83+
) -> Result<Service, Error> {
84+
let metrics_service = Service {
85+
metadata: ObjectMetaBuilder::new()
86+
.name_and_namespace(kafka)
87+
// TODO: Use method on RoleGroupRef once op-rs is released
88+
.name(rolegroup_metrics_service_name(rolegroup))
89+
.ownerreference_from_resource(kafka, None, Some(true))
90+
.context(ObjectMissingMetadataForOwnerRefSnafu)?
91+
.with_recommended_labels(build_recommended_labels(
92+
kafka,
93+
KAFKA_CONTROLLER_NAME,
94+
&resolved_product_image.app_version_label_value,
95+
&rolegroup.role,
96+
&rolegroup.role_group,
97+
))
98+
.context(MetadataBuildSnafu)?
99+
.with_labels(prometheus_labels())
100+
.with_annotations(prometheus_annotations())
101+
.build(),
102+
spec: Some(ServiceSpec {
103+
// Internal communication does not need to be exposed
104+
type_: Some("ClusterIP".to_string()),
105+
cluster_ip: Some("None".to_string()),
106+
ports: Some(metrics_ports()),
107+
selector: Some(
108+
Labels::role_group_selector(
109+
kafka,
110+
APP_NAME,
111+
&rolegroup.role,
112+
&rolegroup.role_group,
113+
)
114+
.context(LabelBuildSnafu)?
115+
.into(),
116+
),
117+
publish_not_ready_addresses: Some(true),
118+
..ServiceSpec::default()
119+
}),
120+
status: None,
121+
};
122+
Ok(metrics_service)
123+
}
124+
125+
/// Headless service for cluster internal purposes only.
126+
// TODO: Move to operator-rs
127+
fn rolegroup_headless_service_name(rolegroup: &RoleGroupRef<v1alpha1::KafkaCluster>) -> String {
128+
format!("{name}-headless", name = rolegroup.object_name())
129+
}
130+
131+
/// Headless metrics service exposes Prometheus endpoint only
132+
// TODO: Move to operator-rs
133+
fn rolegroup_metrics_service_name(rolegroup: &RoleGroupRef<v1alpha1::KafkaCluster>) -> String {
134+
format!("{name}-metrics", name = rolegroup.object_name())
135+
}
136+
137+
fn metrics_ports() -> Vec<ServicePort> {
138+
vec![ServicePort {
139+
name: Some(METRICS_PORT_NAME.to_string()),
140+
port: METRICS_PORT.into(),
141+
protocol: Some("TCP".to_string()),
142+
..ServicePort::default()
143+
}]
144+
}
145+
146+
fn headless_ports(kafka_security: &KafkaTlsSecurity) -> Vec<ServicePort> {
147+
vec![ServicePort {
148+
name: Some(kafka_security.client_port_name().into()),
149+
port: kafka_security.client_port().into(),
150+
protocol: Some("TCP".to_string()),
151+
..ServicePort::default()
152+
}]
153+
}
154+
155+
/// Common labels for Prometheus
156+
fn prometheus_labels() -> Labels {
157+
Labels::try_from([("prometheus.io/scrape", "true")]).expect("should be a valid label")
158+
}
159+
160+
/// Common annotations for Prometheus
161+
///
162+
/// These annotations can be used in a ServiceMonitor.
163+
///
164+
/// see also <https://github.com/prometheus-community/helm-charts/blob/prometheus-27.32.0/charts/prometheus/values.yaml#L983-L1036>
165+
fn prometheus_annotations() -> Annotations {
166+
Annotations::try_from([
167+
("prometheus.io/path".to_owned(), "/metrics".to_owned()),
168+
("prometheus.io/port".to_owned(), METRICS_PORT.to_string()),
169+
("prometheus.io/scheme".to_owned(), "http".to_owned()),
170+
("prometheus.io/scrape".to_owned(), "true".to_owned()),
171+
])
172+
.expect("should be valid annotations")
173+
}

0 commit comments

Comments
 (0)