Skip to content

Commit 5926129

Browse files
committed
feat!: Add dedicated -metrics service
1 parent 689ce07 commit 5926129

10 files changed

Lines changed: 151 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file.
1414
- Support experimental user-info-fetcher Entra backend to fetch user groups ([#712]).
1515
- Add support for OPA `1.4.2` ([#723]).
1616
- Add RBAC rule to helm template for automatic cluster domain detection ([#743]).
17+
- Add a dedicated per-rolegroup `-metrics` Service, which can be used to get Prometheus metrics ([#XXX]).
18+
- Expose more Prometheus metrics, such as successful or failed bundle loads and information about the OPA environment ([#XXX]).
1719

1820
### Changed
1921

@@ -49,6 +51,8 @@ All notable changes to this project will be documented in this file.
4951
- The CLI argument `--kubernetes-node-name` or env variable `KUBERNETES_NODE_NAME` needs to be set. The helm-chart takes care of this.
5052
- The operator helm-chart now grants RBAC `patch` permissions on `events.k8s.io/events`,
5153
so events can be aggregated (e.g. "error happened 10 times over the last 5 minutes") ([#745]).
54+
- The per-rolegroup services now only server the HTTP port and have a `-headless` suffix to better indicate there
55+
purpose and to be consistent with other operators ([#XXX]).
5256

5357
### Fixed
5458

rust/operator-binary/src/controller.rs

Lines changed: 113 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use stackable_operator::{
5050
core::{DeserializeGuard, error_boundary},
5151
runtime::{controller::Action, reflector::ObjectRef},
5252
},
53-
kvp::{Label, LabelError, Labels, ObjectLabels},
53+
kvp::{LabelError, Labels, ObjectLabels},
5454
logging::controller::ReconcilerError,
5555
memory::{BinaryMultiple, MemoryQuantity},
5656
product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config},
@@ -91,6 +91,7 @@ pub const BUNDLES_ACTIVE_DIR: &str = "/bundles/active";
9191
pub const BUNDLES_INCOMING_DIR: &str = "/bundles/incoming";
9292
pub const BUNDLES_TMP_DIR: &str = "/bundles/tmp";
9393
pub const BUNDLE_BUILDER_PORT: i32 = 3030;
94+
pub const OPA_STACKABLE_SERVICE_NAME: &str = "stackable";
9495

9596
const CONFIG_VOLUME_NAME: &str = "config";
9697
const CONFIG_DIR: &str = "/stackable/config";
@@ -185,6 +186,12 @@ pub enum Error {
185186
rolegroup: RoleGroupRef<v1alpha1::OpaCluster>,
186187
},
187188

189+
#[snafu(display("failed to apply metrics Service for [{rolegroup}]"))]
190+
ApplyRoleGroupMetricsService {
191+
source: stackable_operator::cluster_resources::Error,
192+
rolegroup: RoleGroupRef<v1alpha1::OpaCluster>,
193+
},
194+
188195
#[snafu(display("failed to build ConfigMap for [{rolegroup}]"))]
189196
BuildRoleGroupConfig {
190197
source: stackable_operator::builder::configmap::Error,
@@ -337,19 +344,20 @@ pub struct OpaClusterConfigFile {
337344
bundles: OpaClusterBundle,
338345
#[serde(skip_serializing_if = "Option::is_none")]
339346
decision_logs: Option<OpaClusterConfigDecisionLog>,
347+
status: Option<OpaClusterConfigStatus>,
340348
}
341349

342350
impl OpaClusterConfigFile {
343351
pub fn new(decision_logging: Option<OpaClusterConfigDecisionLog>) -> Self {
344352
Self {
345353
services: vec![OpaClusterConfigService {
346-
name: String::from("stackable"),
347-
url: String::from("http://localhost:3030/opa/v1"),
354+
name: OPA_STACKABLE_SERVICE_NAME.to_owned(),
355+
url: "http://localhost:3030/opa/v1".to_owned(),
348356
}],
349357
bundles: OpaClusterBundle {
350358
stackable: OpaClusterBundleConfig {
351-
service: String::from("stackable"),
352-
resource: String::from("opa/bundle.tar.gz"),
359+
service: OPA_STACKABLE_SERVICE_NAME.to_owned(),
360+
resource: "opa/bundle.tar.gz".to_owned(),
353361
persist: true,
354362
polling: OpaClusterBundleConfigPolling {
355363
min_delay_seconds: 10,
@@ -358,6 +366,12 @@ impl OpaClusterConfigFile {
358366
},
359367
},
360368
decision_logs: decision_logging,
369+
// Enable more Prometheus statistics, such as bundle loads
370+
// See https://www.openpolicyagent.org/docs/monitoring#status-metrics
371+
status: Some(OpaClusterConfigStatus {
372+
service: OPA_STACKABLE_SERVICE_NAME.to_owned(),
373+
prometheus: true,
374+
}),
361375
}
362376
}
363377
}
@@ -392,6 +406,12 @@ pub struct OpaClusterConfigDecisionLog {
392406
console: bool,
393407
}
394408

409+
#[derive(Serialize, Deserialize)]
410+
struct OpaClusterConfigStatus {
411+
service: String,
412+
prometheus: bool,
413+
}
414+
395415
pub async fn reconcile_opa(
396416
opa: Arc<DeserializeGuard<v1alpha1::OpaCluster>>,
397417
ctx: Arc<Ctx>,
@@ -489,7 +509,10 @@ pub async fn reconcile_opa(
489509
&rolegroup,
490510
&merged_config,
491511
)?;
492-
let rg_service = build_rolegroup_service(opa, &resolved_product_image, &rolegroup)?;
512+
let rg_service =
513+
build_rolegroup_headless_service(opa, &resolved_product_image, &rolegroup)?;
514+
let rg_metrics_service =
515+
build_rolegroup_metrics_service(opa, &resolved_product_image, &rolegroup)?;
493516
let rg_daemonset = build_server_rolegroup_daemonset(
494517
opa,
495518
&resolved_product_image,
@@ -515,6 +538,12 @@ pub async fn reconcile_opa(
515538
.with_context(|_| ApplyRoleGroupServiceSnafu {
516539
rolegroup: rolegroup.clone(),
517540
})?;
541+
cluster_resources
542+
.add(client, rg_metrics_service)
543+
.await
544+
.with_context(|_| ApplyRoleGroupServiceSnafu {
545+
rolegroup: rolegroup.clone(),
546+
})?;
518547
ds_cond_builder.add(
519548
cluster_resources
520549
.add(client, rg_daemonset.clone())
@@ -632,17 +661,14 @@ pub fn build_server_role_service(
632661
/// The rolegroup [`Service`] is a headless service that allows direct access to the instances of a certain rolegroup
633662
///
634663
/// This is mostly useful for internal communication between peers, or for clients that perform client-side load balancing.
635-
fn build_rolegroup_service(
664+
fn build_rolegroup_headless_service(
636665
opa: &v1alpha1::OpaCluster,
637666
resolved_product_image: &ResolvedProductImage,
638667
rolegroup: &RoleGroupRef<v1alpha1::OpaCluster>,
639668
) -> Result<Service> {
640-
let prometheus_label =
641-
Label::try_from(("prometheus.io/scrape", "true")).context(BuildLabelSnafu)?;
642-
643669
let metadata = ObjectMetaBuilder::new()
644670
.name_and_namespace(opa)
645-
.name(rolegroup.object_name())
671+
.name(rolegroup.rolegroup_headless_service_name())
646672
.ownerreference_from_resource(opa, None, Some(true))
647673
.context(ObjectMissingMetadataForOwnerRefSnafu)?
648674
.with_recommended_labels(build_recommended_labels(
@@ -652,19 +678,20 @@ fn build_rolegroup_service(
652678
&rolegroup.role_group,
653679
))
654680
.context(ObjectMetaSnafu)?
655-
.with_label(prometheus_label)
656681
.build();
657682

658-
let service_selector_labels =
659-
Labels::role_group_selector(opa, APP_NAME, &rolegroup.role, &rolegroup.role_group)
660-
.context(BuildLabelSnafu)?;
661-
662683
let service_spec = ServiceSpec {
663-
// Internal communication does not need to be exposed
684+
// Currently we don't offer listener-exposition of OPA mostly due to security concerns.
685+
// OPA is currently public within the Kubernetes (without authentication).
686+
// Opening it up to outside of Kubernetes might worsen things.
687+
// We are open to implement listener-integration, but this needs to be though through before
688+
// implementing it.
689+
// Note: We have kind of similar situations for HMS and Zookeeper, as the authentication
690+
// options there are non-existent (mTLS still opens plain port) or suck (Kerberos).
664691
type_: Some("ClusterIP".to_string()),
665692
cluster_ip: Some("None".to_string()),
666-
ports: Some(service_ports()),
667-
selector: Some(service_selector_labels.into()),
693+
ports: Some(data_service_ports()),
694+
selector: Some(role_group_selector_labels(opa, rolegroup)?.into()),
668695
publish_not_ready_addresses: Some(true),
669696
..ServiceSpec::default()
670697
};
@@ -676,6 +703,55 @@ fn build_rolegroup_service(
676703
})
677704
}
678705

706+
/// The rolegroup metrics [`Service`] is a service that exposes metrics and has the
707+
/// prometheus.io/scrape label.
708+
fn build_rolegroup_metrics_service(
709+
opa: &v1alpha1::OpaCluster,
710+
resolved_product_image: &ResolvedProductImage,
711+
rolegroup: &RoleGroupRef<v1alpha1::OpaCluster>,
712+
) -> Result<Service> {
713+
let labels = Labels::try_from([("prometheus.io/scrape", "true")])
714+
.expect("static Prometheus labels must be valid");
715+
716+
let metadata = ObjectMetaBuilder::new()
717+
.name_and_namespace(opa)
718+
.name(rolegroup.rolegroup_metrics_service_name())
719+
.ownerreference_from_resource(opa, None, Some(true))
720+
.context(ObjectMissingMetadataForOwnerRefSnafu)?
721+
.with_recommended_labels(build_recommended_labels(
722+
opa,
723+
&resolved_product_image.app_version_label,
724+
&rolegroup.role,
725+
&rolegroup.role_group,
726+
))
727+
.context(ObjectMetaSnafu)?
728+
.with_labels(labels)
729+
.build();
730+
731+
let service_spec = ServiceSpec {
732+
type_: Some("ClusterIP".to_string()),
733+
cluster_ip: Some("None".to_string()),
734+
ports: Some(vec![metrics_service_port()]),
735+
selector: Some(role_group_selector_labels(opa, rolegroup)?.into()),
736+
..ServiceSpec::default()
737+
};
738+
739+
Ok(Service {
740+
metadata,
741+
spec: Some(service_spec),
742+
status: None,
743+
})
744+
}
745+
746+
/// Returns the [`Labels`] that can be used to select all Pods that are part of the roleGroup.
747+
fn role_group_selector_labels(
748+
opa: &v1alpha1::OpaCluster,
749+
rolegroup: &RoleGroupRef<v1alpha1::OpaCluster>,
750+
) -> Result<Labels> {
751+
Labels::role_group_selector(opa, APP_NAME, &rolegroup.role, &rolegroup.role_group)
752+
.context(BuildLabelSnafu)
753+
}
754+
679755
/// The rolegroup [`ConfigMap`] configures the rolegroup based on the configuration given by the administrator
680756
fn build_server_rolegroup_config_map(
681757
opa: &v1alpha1::OpaCluster,
@@ -1387,22 +1463,24 @@ fn build_prepare_start_command(
13871463
prepare_container_args
13881464
}
13891465

1390-
fn service_ports() -> Vec<ServicePort> {
1391-
vec![
1392-
ServicePort {
1393-
name: Some(APP_PORT_NAME.to_string()),
1394-
port: APP_PORT.into(),
1395-
protocol: Some("TCP".to_string()),
1396-
..ServicePort::default()
1397-
},
1398-
ServicePort {
1399-
name: Some(METRICS_PORT_NAME.to_string()),
1400-
port: 9504, // Arbitrary port number, this is never actually used anywhere
1401-
protocol: Some("TCP".to_string()),
1402-
target_port: Some(IntOrString::String(APP_PORT_NAME.to_string())),
1403-
..ServicePort::default()
1404-
},
1405-
]
1466+
fn data_service_ports() -> Vec<ServicePort> {
1467+
// Currently only HTTP is exposed
1468+
vec![ServicePort {
1469+
name: Some(APP_PORT_NAME.to_string()),
1470+
port: APP_PORT.into(),
1471+
protocol: Some("TCP".to_string()),
1472+
..ServicePort::default()
1473+
}]
1474+
}
1475+
1476+
fn metrics_service_port() -> ServicePort {
1477+
ServicePort {
1478+
name: Some(METRICS_PORT_NAME.to_string()),
1479+
// The metrics are served on the same port as the HTTP traffic
1480+
port: APP_PORT.into(),
1481+
protocol: Some("TCP".to_string()),
1482+
..ServicePort::default()
1483+
}
14061484
}
14071485

14081486
/// Creates recommended `ObjectLabels` to be used in deployed resources

tests/templates/kuttl/smoke/20-assert.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ timeout: 300
66
apiVersion: apps/v1
77
kind: StatefulSet
88
metadata:
9-
name: test-regorule
9+
name: test-opa
1010
status:
1111
readyReplicas: 1
1212
replicas: 1

tests/templates/kuttl/smoke/20-install-test-regorule.yaml renamed to tests/templates/kuttl/smoke/20-install-test-opa.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22
apiVersion: apps/v1
33
kind: StatefulSet
44
metadata:
5-
name: test-regorule
5+
name: test-opa
66
labels:
7-
app: test-regorule
7+
app: test-opa
88
spec:
99
replicas: 1
1010
selector:
1111
matchLabels:
12-
app: test-regorule
12+
app: test-opa
1313
template:
1414
metadata:
1515
labels:
16-
app: test-regorule
16+
app: test-opa
1717
spec:
1818
containers:
19-
- name: test-regorule
19+
- name: test-opa
2020
image: oci.stackable.tech/sdp/testing-tools:0.2.0-stackable0.0.0-dev
2121
stdin: true
2222
tty: true

tests/templates/kuttl/smoke/30-assert.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ kind: TestAssert
44
metadata:
55
name: test-regorule
66
commands:
7-
- script: kubectl exec -n $NAMESPACE test-regorule-0 -- python /tmp/test-regorule.py -u 'http://test-opa-server-default:8081/v1/data/test'
7+
- script: kubectl exec -n $NAMESPACE test-opa-0 -- python /tmp/30_test-regorule.py -u 'http://test-opa:8081/v1/data/test'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: kuttl.dev/v1beta1
2+
kind: TestStep
3+
commands:
4+
- script: kubectl cp -n $NAMESPACE ./30_test-regorule.py test-opa-0:/tmp
5+
- script: kubectl cp -n $NAMESPACE ./30_test-metrics.py test-opa-0:/tmp

tests/templates/kuttl/smoke/30-prepare-test-regorule.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python
2+
import requests
3+
4+
# metrics_url = "http://test-opa-server-default:9504/metrics"
5+
metrics_url = "http://test-opa-server-default-metrics:8081/metrics"
6+
# FIXME: Ideally this would be exposed via a metrics service (as the other operators do)
7+
response = requests.get(metrics_url)
8+
9+
assert response.status_code == 200, "Metrics endpoint must return a 200 status code"
10+
assert "bundle_loaded_counter" in response.text, f"Metric bundle_loaded_counter should exist in {metrics_url}"
11+
12+
print("Metrics test successful!")

tests/templates/kuttl/smoke/test-regorule.py renamed to tests/templates/kuttl/smoke/30_test-regorule.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
and "hello" in response["result"]
3535
and response["result"]["hello"]
3636
):
37-
print("Test successful!")
37+
print("Regorule test successful!")
3838
exit(0)
3939
else:
4040
print(
@@ -43,3 +43,5 @@
4343
+ " - expected: {'result': {'hello': True}}"
4444
)
4545
exit(-1)
46+
47+
metrics = requests.get(f"{url}/metrics")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
apiVersion: kuttl.dev/v1beta1
3+
kind: TestAssert
4+
metadata:
5+
name: test-metrics
6+
commands:
7+
- script: kubectl exec -n $NAMESPACE test-opa-0 -- python /tmp/30_test-metrics.py

0 commit comments

Comments
 (0)