Skip to content

Commit

Permalink
add podmonitor to coredb spec (#809)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChuckHend authored May 29, 2024
1 parent 0aee059 commit d507f10
Show file tree
Hide file tree
Showing 11 changed files with 1,286 additions and 13 deletions.
2 changes: 1 addition & 1 deletion charts/tembo-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: tembo-operator
description: "Helm chart to deploy the tembo-operator"
type: application
icon: https://cloud.tembo.io/images/TemboElephant.png
version: 0.5.3
version: 0.6.0
home: https://tembo.io
sources:
- https://github.com/tembo-io/tembo
Expand Down
16 changes: 16 additions & 0 deletions charts/tembo-operator/templates/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,22 @@ spec:
image:
description: Defines the container image to use for the appService.
type: string
metrics:
description: Defines the metrics endpoints to be scraped by Prometheus. This implements a subset of features available by PodMonitorPodMetricsEndpoints.
nullable: true
properties:
path:
description: path to scrape metrics
type: string
port:
description: port must be also exposed in one of AppService.routing[]
format: uint16
minimum: 0.0
type: integer
required:
- path
- port
type: object
middlewares:
description: Defines the ingress middeware configuration for the appService. This is specifically configured for the ingress controller Traefik.
items:
Expand Down
2 changes: 1 addition & 1 deletion tembo-operator/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tembo-operator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "controller"
description = "Tembo Operator for Postgres"
version = "0.47.3"
version = "0.48.0"
edition = "2021"
default-run = "controller"
license = "Apache-2.0"
Expand Down
106 changes: 102 additions & 4 deletions tembo-operator/src/app_service/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ use super::{

use crate::{app_service::types::IngressType, secret::fetch_all_decoded_data_from_secret};

const APP_CONTAINER_PORT_PREFIX: &str = "app-";

// private wrapper to hold the AppService Resources
#[derive(Clone, Debug)]
struct AppServiceResources {
Expand All @@ -48,6 +50,7 @@ struct AppServiceResources {
ingress_tcp_routes: Option<Vec<IngressRouteTCPRoutes>>,
entry_points: Option<Vec<String>>,
entry_points_tcp: Option<Vec<String>>,
podmonitor: Option<podmon::PodMonitor>,
}

// generates Kubernetes Deployment and Service templates for a AppService
Expand Down Expand Up @@ -76,11 +79,13 @@ fn generate_resource(
coredb_name,
&resource_name,
namespace,
oref,
oref.clone(),
annotations,
placement,
placement.clone(),
);

let maybe_podmonitor = generate_podmonitor(appsvc, &resource_name, namespace, annotations);

// If DATA_PLANE_BASEDOMAIN is not set, don't generate IngressRoutes, IngressRouteTCPs, or EntryPoints
if domain.is_none() {
return AppServiceResources {
Expand All @@ -91,6 +96,7 @@ fn generate_resource(
ingress_tcp_routes: None,
entry_points: None,
entry_points_tcp: None,
podmonitor: maybe_podmonitor,
};
}
// It's safe to unwrap domain here because we've already checked if it's None
Expand Down Expand Up @@ -158,6 +164,7 @@ fn generate_resource(
ingress_tcp_routes,
entry_points,
entry_points_tcp,
podmonitor: maybe_podmonitor,
}
}

Expand Down Expand Up @@ -194,7 +201,7 @@ fn generate_service(
port: p as i32,
// there can be more than one ServicePort per Service
// these must be unique, so we'll use the port number
name: Some(format!("http-{}", p)),
name: Some(format!("{APP_CONTAINER_PORT_PREFIX}{p}")),
target_port: None,
..ServicePort::default()
})
Expand Down Expand Up @@ -279,6 +286,7 @@ fn generate_deployment(
let container_ports: Vec<ContainerPort> = distinct_ports
.into_iter()
.map(|p| ContainerPort {
name: Some(format!("{APP_CONTAINER_PORT_PREFIX}{p}")),
container_port: p as i32,
protocol: Some("TCP".to_string()),
..ContainerPort::default()
Expand Down Expand Up @@ -585,7 +593,6 @@ pub fn to_delete(desired: Vec<String>, actual: Vec<String>) -> Option<Vec<String

async fn apply_resources(resources: Vec<AppServiceResources>, client: &Client, ns: &str) -> bool {
let deployment_api: Api<Deployment> = Api::namespaced(client.clone(), ns);
let service_api: Api<Service> = Api::namespaced(client.clone(), ns);
let ps = PatchParams::apply("cntrlr").force();

let mut has_errors: bool = false;
Expand All @@ -612,6 +619,8 @@ async fn apply_resources(resources: Vec<AppServiceResources>, client: &Client, n
if res.service.is_none() {
continue;
}

let service_api: Api<Service> = Api::namespaced(client.clone(), ns);
match service_api
.patch(&res.name, &ps, &Patch::Apply(&res.service))
.await
Expand All @@ -629,6 +638,50 @@ async fn apply_resources(resources: Vec<AppServiceResources>, client: &Client, n
);
}
}

let podmon_api: Api<podmon::PodMonitor> = Api::namespaced(client.clone(), ns);
if let Some(mut pmon) = res.podmonitor {
// assign ownership of the PodMonitor to the Service
// if Service is deleted, so is the PodMonitor
let meta = service_api.get(&res.name).await;
if let Ok(svc) = meta {
let uid = svc.metadata.uid.unwrap_or_default();
let oref = OwnerReference {
api_version: "v1".to_string(),
kind: "Service".to_string(),
name: res.name.clone(),
uid,
controller: Some(true),
block_owner_deletion: Some(true),
};
pmon.metadata.owner_references = Some(vec![oref]);
}
match podmon_api
.patch(&res.name, &ps, &Patch::Apply(&pmon))
.await
.map_err(Error::KubeError)
{
Ok(_) => {
debug!("ns: {}, applied PodMonitor: {}", ns, res.name);
}
Err(e) => {
has_errors = true;
error!(
"ns: {}, failed to apply PodMonitor for AppService: {}, error: {}",
ns, res.name, e
);
}
}
} else {
match podmon_api.delete(&res.name, &Default::default()).await.ok() {
Some(_) => {
debug!("ns: {}, deleted PodMonitor: {}", ns, res.name);
}
None => {
debug!("ns: {}, PodMonitor does not exist: {}", ns, res.name);
}
}
}
}
has_errors
}
Expand Down Expand Up @@ -947,6 +1000,51 @@ pub async fn prepare_apps_connection_secret(client: Client, cdb: &CoreDB) -> Res
Ok(())
}

use crate::prometheus::podmonitor_crd as podmon;

fn generate_podmonitor(
appsvc: &AppService,
resource_name: &str,
namespace: &str,
annotations: &BTreeMap<String, String>,
) -> Option<podmon::PodMonitor> {
let metrics = appsvc.metrics.clone()?;

let mut selector_labels: BTreeMap<String, String> = BTreeMap::new();
selector_labels.insert("app".to_owned(), resource_name.to_string());

let mut labels = selector_labels.clone();
labels.insert("component".to_owned(), COMPONENT_NAME.to_owned());
labels.insert("coredb.io/name".to_owned(), namespace.to_owned());

let podmon_metadata = ObjectMeta {
name: Some(resource_name.to_string()),
namespace: Some(namespace.to_owned()),
labels: Some(labels.clone()),
annotations: Some(annotations.clone()),
..ObjectMeta::default()
};

let metrics_endpoint = podmon::PodMonitorPodMetricsEndpoints {
path: Some(metrics.path),
port: Some(format!("{APP_CONTAINER_PORT_PREFIX}{}", metrics.port)),
..podmon::PodMonitorPodMetricsEndpoints::default()
};

let pmonspec = podmon::PodMonitorSpec {
pod_metrics_endpoints: Some(vec![metrics_endpoint]),
selector: podmon::PodMonitorSelector {
match_labels: Some(selector_labels.clone()),
..podmon::PodMonitorSelector::default()
},
..podmon::PodMonitorSpec::default()
};
Some(podmon::PodMonitor {
metadata: podmon_metadata,
spec: pmonspec,
})
}

#[cfg(test)]
mod tests {
use crate::{apis::coredb_types::CoreDB, app_service::manager::generate_appsvc_annotations};
Expand Down
12 changes: 12 additions & 0 deletions tembo-operator/src/app_service/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ pub struct AppService {
/// See the [Kubernetes docs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).
pub probes: Option<Probes>,

/// Defines the metrics endpoints to be scraped by Prometheus.
/// This implements a subset of features available by PodMonitorPodMetricsEndpoints.
pub metrics: Option<AppMetrics>,

/// Defines the ingress middeware configuration for the appService.
/// This is specifically configured for the ingress controller Traefik.
pub middlewares: Option<Vec<Middleware>>,
Expand All @@ -146,6 +150,14 @@ pub fn default_resources() -> ResourceRequirements {
}
}

#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
pub struct AppMetrics {
/// port must be also exposed in one of AppService.routing[]
pub port: u16,
/// path to scrape metrics
pub path: String,
}

// Secrets are injected into the container as environment variables
// ths allows users to map these secrets to environment variable of their choice
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, JsonSchema, PartialEq)]
Expand Down
1 change: 1 addition & 0 deletions tembo-operator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use metrics::Metrics;
mod config;
pub mod defaults;
pub mod errors;
pub mod prometheus;

pub mod cloudnativepg;
mod deployment_postgres_exporter;
Expand Down
1 change: 1 addition & 0 deletions tembo-operator/src/prometheus/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod podmonitor_crd;
Loading

0 comments on commit d507f10

Please sign in to comment.