Compare commits
7 Commits
feat/unshe
...
78e595e696
| Author | SHA1 | Date | |
|---|---|---|---|
| 78e595e696 | |||
| 90b89224d8 | |||
| 759a9287d3 | |||
| 24922321b1 | |||
| cf84f2cce8 | |||
| a12d12aa4f | |||
| cefb65933a |
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -1804,6 +1804,25 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "example-okd-cluster-alerts"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"brocade",
|
||||||
|
"cidr",
|
||||||
|
"env_logger",
|
||||||
|
"harmony",
|
||||||
|
"harmony_cli",
|
||||||
|
"harmony_macros",
|
||||||
|
"harmony_secret",
|
||||||
|
"harmony_secret_derive",
|
||||||
|
"harmony_types",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "example-okd-install"
|
name = "example-okd-install"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_types::net::Url;
|
use harmony_types::{k8s_name::K8sName, net::Url};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: K8sName("test-discord".to_string()),
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
|
selectors: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ use harmony::{
|
|||||||
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
|
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use harmony_types::id::Id;
|
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
|
use harmony_types::{id::Id, k8s_name::K8sName};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -43,8 +43,9 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: K8sName("test-discord".to_string()),
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
|
selectors: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
||||||
|
|||||||
22
examples/okd_cluster_alerts/Cargo.toml
Normal file
22
examples/okd_cluster_alerts/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "example-okd-cluster-alerts"
|
||||||
|
edition = "2024"
|
||||||
|
version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
harmony = { path = "../../harmony" }
|
||||||
|
harmony_cli = { path = "../../harmony_cli" }
|
||||||
|
harmony_types = { path = "../../harmony_types" }
|
||||||
|
harmony_secret = { path = "../../harmony_secret" }
|
||||||
|
harmony_secret_derive = { path = "../../harmony_secret_derive" }
|
||||||
|
cidr = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
harmony_macros = { path = "../../harmony_macros" }
|
||||||
|
log = { workspace = true }
|
||||||
|
env_logger = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
serde.workspace = true
|
||||||
|
brocade = { path = "../../brocade" }
|
||||||
38
examples/okd_cluster_alerts/src/main.rs
Normal file
38
examples/okd_cluster_alerts/src/main.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use harmony::{
|
||||||
|
inventory::Inventory,
|
||||||
|
modules::monitoring::{
|
||||||
|
alert_channel::discord_alert_channel::DiscordWebhook,
|
||||||
|
okd::cluster_monitoring::OpenshiftClusterAlertScore,
|
||||||
|
},
|
||||||
|
topology::K8sAnywhereTopology,
|
||||||
|
};
|
||||||
|
use harmony_macros::hurl;
|
||||||
|
use harmony_types::k8s_name::K8sName;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let mut sel = HashMap::new();
|
||||||
|
sel.insert(
|
||||||
|
"openshift_io_alert_source".to_string(),
|
||||||
|
"platform".to_string(),
|
||||||
|
);
|
||||||
|
let mut sel2 = HashMap::new();
|
||||||
|
sel2.insert("openshift_io_alert_source".to_string(), "".to_string());
|
||||||
|
let selectors = vec![sel, sel2];
|
||||||
|
harmony_cli::run(
|
||||||
|
Inventory::autoload(),
|
||||||
|
K8sAnywhereTopology::from_env(),
|
||||||
|
vec![Box::new(OpenshiftClusterAlertScore {
|
||||||
|
receivers: vec![Box::new(DiscordWebhook {
|
||||||
|
name: K8sName("wills-discord-webhook-example".to_string()),
|
||||||
|
url: hurl!("https://something.io"),
|
||||||
|
selectors: selectors,
|
||||||
|
})],
|
||||||
|
})],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
@@ -10,7 +10,7 @@ use harmony::{
|
|||||||
},
|
},
|
||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_types::net::Url;
|
use harmony_types::{k8s_name::K8sName, net::Url};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -22,8 +22,9 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: K8sName("test-discord".to_string()),
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||||
|
selectors: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = ApplicationScore {
|
let app = ApplicationScore {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
@@ -14,6 +14,7 @@ use harmony::{
|
|||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
use harmony_macros::hurl;
|
||||||
|
use harmony_types::k8s_name::K8sName;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -25,8 +26,9 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: K8sName("test-discord".to_string()),
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
url: hurl!("https://discord.doesnt.exist.com"),
|
||||||
|
selectors: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
let webhook_receiver = WebhookReceiver {
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
apiVersion: v2
|
||||||
|
name: harmony-example-rust-webapp-chart
|
||||||
|
description: A Helm chart for the harmony-example-rust-webapp web application.
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "latest"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
*/}}
|
||||||
|
{{- define "chart.fullname" -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: {{ include "chart.name" . }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: {{ include "chart.name" . }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3000
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml .Values.ingress.annotations | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "chart.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: 3000
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: 3000
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: {{ include "chart.name" . }}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
# Default values for harmony-example-rust-webapp-chart.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: hub.nationtech.io/harmony/harmony-example-rust-webapp
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overridden by the chart's appVersion
|
||||||
|
tag: "latest"
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3000
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
# Annotations for cert-manager to handle SSL.
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
# Add other annotations like nginx ingress class if needed
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls:
|
||||||
|
- secretName: harmony-example-rust-webapp-tls
|
||||||
|
hosts:
|
||||||
|
- chart-example.local
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ use harmony::{
|
|||||||
topology::K8sAnywhereTopology,
|
topology::K8sAnywhereTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::hurl;
|
use harmony_macros::hurl;
|
||||||
|
use harmony_types::k8s_name::K8sName;
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -31,8 +32,9 @@ async fn main() {
|
|||||||
Box::new(Monitoring {
|
Box::new(Monitoring {
|
||||||
application: application.clone(),
|
application: application.clone(),
|
||||||
alert_receiver: vec![Box::new(DiscordWebhook {
|
alert_receiver: vec![Box::new(DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: K8sName("test-discord".to_string()),
|
||||||
url: hurl!("https://discord.doesnt.exist.com"),
|
url: hurl!("https://discord.doesnt.exist.com"),
|
||||||
|
selectors: vec![],
|
||||||
})],
|
})],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use kube::{
|
|||||||
Client, Config, Discovery, Error, Resource,
|
Client, Config, Discovery, Error, Resource,
|
||||||
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
||||||
config::{KubeConfigOptions, Kubeconfig},
|
config::{KubeConfigOptions, Kubeconfig},
|
||||||
core::ErrorResponse,
|
core::{DynamicResourceScope, ErrorResponse},
|
||||||
discovery::{ApiCapabilities, Scope},
|
discovery::{ApiCapabilities, Scope},
|
||||||
error::DiscoveryError,
|
error::DiscoveryError,
|
||||||
runtime::reflector::Lookup,
|
runtime::reflector::Lookup,
|
||||||
@@ -94,6 +94,23 @@ impl K8sClient {
|
|||||||
resource.get(name).await
|
resource.get(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_secret_json_value(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
) -> Result<DynamicObject, Error> {
|
||||||
|
self.get_resource_json_value(
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
&GroupVersionKind {
|
||||||
|
group: "".to_string(),
|
||||||
|
version: "v1".to_string(),
|
||||||
|
kind: "Secret".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_deployment(
|
pub async fn get_deployment(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -337,6 +354,169 @@ impl K8sClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_api_for_dynamic_object(
|
||||||
|
&self,
|
||||||
|
object: &DynamicObject,
|
||||||
|
ns: Option<&str>,
|
||||||
|
) -> Result<Api<DynamicObject>, Error> {
|
||||||
|
let api_resource = object
|
||||||
|
.types
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| {
|
||||||
|
let parts: Vec<&str> = t.api_version.split('/').collect();
|
||||||
|
match parts.as_slice() {
|
||||||
|
[version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk(
|
||||||
|
"", version, &t.kind,
|
||||||
|
))),
|
||||||
|
[group, version] => Some(ApiResource::from_gvk(&GroupVersionKind::gvk(
|
||||||
|
group, version, &t.kind,
|
||||||
|
))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::BuildRequest(kube::core::request::Error::Validation(
|
||||||
|
"Invalid apiVersion in DynamicObject {object:#?}".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match ns {
|
||||||
|
Some(ns) => Ok(Api::namespaced_with(self.client.clone(), ns, &api_resource)),
|
||||||
|
None => Ok(Api::default_namespaced_with(
|
||||||
|
self.client.clone(),
|
||||||
|
&api_resource,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_dynamic_many(
|
||||||
|
&self,
|
||||||
|
resource: &[DynamicObject],
|
||||||
|
namespace: Option<&str>,
|
||||||
|
force_conflicts: bool,
|
||||||
|
) -> Result<Vec<DynamicObject>, Error> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for r in resource.iter() {
|
||||||
|
result.push(self.apply_dynamic(r, namespace, force_conflicts).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply DynamicObject resource to the cluster
|
||||||
|
pub async fn apply_dynamic(
|
||||||
|
&self,
|
||||||
|
resource: &DynamicObject,
|
||||||
|
namespace: Option<&str>,
|
||||||
|
force_conflicts: bool,
|
||||||
|
) -> Result<DynamicObject, Error> {
|
||||||
|
// Build API for this dynamic object
|
||||||
|
let api = self.get_api_for_dynamic_object(resource, namespace)?;
|
||||||
|
let name = resource
|
||||||
|
.metadata
|
||||||
|
.name
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::BuildRequest(kube::core::request::Error::Validation(
|
||||||
|
"DynamicObject must have metadata.name".to_string(),
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.as_str();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Applying dynamic resource kind={:?} apiVersion={:?} name='{}' ns={:?}",
|
||||||
|
resource.types.as_ref().map(|t| &t.kind),
|
||||||
|
resource.types.as_ref().map(|t| &t.api_version),
|
||||||
|
name,
|
||||||
|
namespace
|
||||||
|
);
|
||||||
|
trace!(
|
||||||
|
"Dynamic resource payload:\n{:#}",
|
||||||
|
serde_json::to_value(resource).unwrap_or(serde_json::Value::Null)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Using same field manager as in apply()
|
||||||
|
let mut patch_params = PatchParams::apply("harmony");
|
||||||
|
patch_params.force = force_conflicts;
|
||||||
|
|
||||||
|
if *crate::config::DRY_RUN {
|
||||||
|
// Dry-run path: fetch current, show diff, and return appropriate object
|
||||||
|
match api.get(name).await {
|
||||||
|
Ok(current) => {
|
||||||
|
trace!("Received current dynamic value {current:#?}");
|
||||||
|
|
||||||
|
println!("\nPerforming dry-run for resource: '{}'", name);
|
||||||
|
|
||||||
|
// Serialize current and new, and strip status from current if present
|
||||||
|
let mut current_yaml =
|
||||||
|
serde_yaml::to_value(¤t).unwrap_or_else(|_| serde_yaml::Value::Null);
|
||||||
|
if let Some(map) = current_yaml.as_mapping_mut() {
|
||||||
|
if map.contains_key(&serde_yaml::Value::String("status".to_string())) {
|
||||||
|
let removed =
|
||||||
|
map.remove(&serde_yaml::Value::String("status".to_string()));
|
||||||
|
trace!("Removed status from current dynamic object: {:?}", removed);
|
||||||
|
} else {
|
||||||
|
trace!(
|
||||||
|
"Did not find status entry for current dynamic object {}/{}",
|
||||||
|
current.metadata.namespace.as_deref().unwrap_or(""),
|
||||||
|
current.metadata.name.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_yaml = serde_yaml::to_string(¤t_yaml)
|
||||||
|
.unwrap_or_else(|_| "Failed to serialize current resource".to_string());
|
||||||
|
let new_yaml = serde_yaml::to_string(resource)
|
||||||
|
.unwrap_or_else(|_| "Failed to serialize new resource".to_string());
|
||||||
|
|
||||||
|
if current_yaml == new_yaml {
|
||||||
|
println!("No changes detected.");
|
||||||
|
return Ok(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Changes detected:");
|
||||||
|
let diff = TextDiff::from_lines(¤t_yaml, &new_yaml);
|
||||||
|
for change in diff.iter_all_changes() {
|
||||||
|
let sign = match change.tag() {
|
||||||
|
similar::ChangeTag::Delete => "-",
|
||||||
|
similar::ChangeTag::Insert => "+",
|
||||||
|
similar::ChangeTag::Equal => " ",
|
||||||
|
};
|
||||||
|
print!("{}{}", sign, change);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the incoming resource as the would-be applied state
|
||||||
|
Ok(resource.clone())
|
||||||
|
}
|
||||||
|
Err(Error::Api(ErrorResponse { code: 404, .. })) => {
|
||||||
|
println!("\nPerforming dry-run for new resource: '{}'", name);
|
||||||
|
println!(
|
||||||
|
"Resource does not exist. It would be created with the following content:"
|
||||||
|
);
|
||||||
|
let new_yaml = serde_yaml::to_string(resource)
|
||||||
|
.unwrap_or_else(|_| "Failed to serialize new resource".to_string());
|
||||||
|
for line in new_yaml.lines() {
|
||||||
|
println!("+{}", line);
|
||||||
|
}
|
||||||
|
Ok(resource.clone())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get dynamic resource '{}': {}", name, e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Real apply via server-side apply
|
||||||
|
debug!("Patching (server-side apply) dynamic resource '{}'", name);
|
||||||
|
api.patch(name, &patch_params, &Patch::Apply(resource))
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to apply dynamic resource '{}': {}", name, e);
|
||||||
|
e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a resource in namespace
|
/// Apply a resource in namespace
|
||||||
///
|
///
|
||||||
/// See `kubectl apply` for more information on the expected behavior of this function
|
/// See `kubectl apply` for more information on the expected behavior of this function
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::any::Any;
|
use std::{any::Any, collections::HashMap};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use kube::api::DynamicObject;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -76,6 +77,15 @@ pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
|||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AlertManagerReceiver {
|
||||||
|
pub receiver_config: serde_json::Value,
|
||||||
|
// FIXME we should not leak k8s here. DynamicObject is k8s specific
|
||||||
|
pub additional_ressources: Vec<DynamicObject>,
|
||||||
|
pub route_config: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use k8s_openapi::{
|
|||||||
},
|
},
|
||||||
apimachinery::pkg::util::intstr::IntOrString,
|
apimachinery::pkg::util::intstr::IntOrString,
|
||||||
};
|
};
|
||||||
use kube::Resource;
|
use kube::{Resource, api::DynamicObject};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use harmony_types::k8s_name::K8sName;
|
||||||
use k8s_openapi::api::core::v1::Secret;
|
use k8s_openapi::api::core::v1::Secret;
|
||||||
use kube::api::ObjectMeta;
|
use kube::Resource;
|
||||||
use log::debug;
|
use kube::api::{DynamicObject, ObjectMeta};
|
||||||
|
use log::{debug, trace};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
|
|
||||||
|
use crate::infra::kube::kube_resource_to_dynamic;
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{
|
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{
|
||||||
AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus,
|
AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus,
|
||||||
};
|
};
|
||||||
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability;
|
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability;
|
||||||
|
use crate::modules::monitoring::okd::OpenshiftClusterAlertSender;
|
||||||
|
use crate::topology::oberservability::monitoring::AlertManagerReceiver;
|
||||||
use crate::{
|
use crate::{
|
||||||
interpret::{InterpretError, Outcome},
|
interpret::{InterpretError, Outcome},
|
||||||
modules::monitoring::{
|
modules::monitoring::{
|
||||||
@@ -28,14 +33,13 @@ use harmony_types::net::Url;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct DiscordWebhook {
|
pub struct DiscordWebhook {
|
||||||
pub name: String,
|
pub name: K8sName,
|
||||||
pub url: Url,
|
pub url: Url,
|
||||||
|
pub selectors: Vec<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl DiscordWebhook {
|
||||||
impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
fn get_receiver_config(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
|
||||||
let ns = sender.namespace.clone();
|
|
||||||
let secret_name = format!("{}-secret", self.name.clone());
|
let secret_name = format!("{}-secret", self.name.clone());
|
||||||
let webhook_key = format!("{}", self.url.clone());
|
let webhook_key = format!("{}", self.url.clone());
|
||||||
|
|
||||||
@@ -52,33 +56,91 @@ impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = sender.client.apply(&secret, Some(&ns)).await;
|
let mut matchers: Vec<String> = Vec::new();
|
||||||
|
for selector in &self.selectors {
|
||||||
|
trace!("selector: {:#?}", selector);
|
||||||
|
for (k, v) in selector {
|
||||||
|
matchers.push(format!("{} = {}", k, v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AlertManagerReceiver {
|
||||||
|
additional_ressources: vec![kube_resource_to_dynamic(&secret)?],
|
||||||
|
|
||||||
|
receiver_config: json!({
|
||||||
|
"name": self.name,
|
||||||
|
"discord_configs": [
|
||||||
|
{
|
||||||
|
"webhook_url": self.url.clone(),
|
||||||
|
"title": "{{ template \"discord.default.title\" . }}",
|
||||||
|
"message": "{{ template \"discord.default.message\" . }}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
route_config: json!({
|
||||||
|
"receiver": self.name,
|
||||||
|
"matchers": matchers,
|
||||||
|
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AlertReceiver<OpenshiftClusterAlertSender> for DiscordWebhook {
|
||||||
|
async fn install(
|
||||||
|
&self,
|
||||||
|
sender: &OpenshiftClusterAlertSender,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
self.name.clone().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_box(&self) -> Box<dyn AlertReceiver<OpenshiftClusterAlertSender>> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
self.get_receiver_config()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
||||||
|
let ns = sender.namespace.clone();
|
||||||
|
|
||||||
|
let config = self.get_receiver_config()?;
|
||||||
|
for resource in config.additional_ressources.iter() {
|
||||||
|
todo!("can I apply a dynamicresource");
|
||||||
|
// sender.client.apply(resource, Some(&ns)).await;
|
||||||
|
}
|
||||||
|
|
||||||
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
||||||
data: json!({
|
data: json!({
|
||||||
"route": {
|
"route": {
|
||||||
"receiver": self.name,
|
"receiver": self.name,
|
||||||
},
|
},
|
||||||
"receivers": [
|
"receivers": [
|
||||||
{
|
config.receiver_config
|
||||||
"name": self.name,
|
|
||||||
"discordConfigs": [
|
|
||||||
{
|
|
||||||
"apiURL": {
|
|
||||||
"name": secret_name,
|
|
||||||
"key": "webhook-url",
|
|
||||||
},
|
|
||||||
"title": "{{ template \"discord.default.title\" . }}",
|
|
||||||
"message": "{{ template \"discord.default.message\" . }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfig {
|
let alertmanager_configs = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfig {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
name: Some(self.name.clone()),
|
name: Some(self.name.clone().to_string()),
|
||||||
labels: Some(std::collections::BTreeMap::from([(
|
labels: Some(std::collections::BTreeMap::from([(
|
||||||
"alertmanagerConfig".to_string(),
|
"alertmanagerConfig".to_string(),
|
||||||
"enabled".to_string(),
|
"enabled".to_string(),
|
||||||
@@ -122,6 +184,9 @@ impl AlertReceiver<RHOBObservability> for DiscordWebhook {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
||||||
let ns = sender.namespace.clone();
|
let ns = sender.namespace.clone();
|
||||||
let secret_name = format!("{}-secret", self.name.clone());
|
let secret_name = format!("{}-secret", self.name.clone());
|
||||||
@@ -167,7 +232,7 @@ impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
|||||||
|
|
||||||
let alertmanager_configs = AlertmanagerConfig {
|
let alertmanager_configs = AlertmanagerConfig {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
name: Some(self.name.clone()),
|
name: Some(self.name.clone().to_string()),
|
||||||
labels: Some(std::collections::BTreeMap::from([(
|
labels: Some(std::collections::BTreeMap::from([(
|
||||||
"alertmanagerConfig".to_string(),
|
"alertmanagerConfig".to_string(),
|
||||||
"enabled".to_string(),
|
"enabled".to_string(),
|
||||||
@@ -200,6 +265,9 @@ impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<Prometheus> for DiscordWebhook {
|
impl AlertReceiver<Prometheus> for DiscordWebhook {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
@@ -217,7 +285,7 @@ impl AlertReceiver<Prometheus> for DiscordWebhook {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl PrometheusReceiver for DiscordWebhook {
|
impl PrometheusReceiver for DiscordWebhook {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
self.name.clone()
|
self.name.clone().to_string()
|
||||||
}
|
}
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
||||||
self.get_config().await
|
self.get_config().await
|
||||||
@@ -226,6 +294,9 @@ impl PrometheusReceiver for DiscordWebhook {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<KubePrometheus> for DiscordWebhook {
|
impl AlertReceiver<KubePrometheus> for DiscordWebhook {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
@@ -243,7 +314,7 @@ impl AlertReceiver<KubePrometheus> for DiscordWebhook {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl KubePrometheusReceiver for DiscordWebhook {
|
impl KubePrometheusReceiver for DiscordWebhook {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
self.name.clone()
|
self.name.clone().to_string()
|
||||||
}
|
}
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
||||||
self.get_config().await
|
self.get_config().await
|
||||||
@@ -270,7 +341,7 @@ impl DiscordWebhook {
|
|||||||
let mut route = Mapping::new();
|
let mut route = Mapping::new();
|
||||||
route.insert(
|
route.insert(
|
||||||
Value::String("receiver".to_string()),
|
Value::String("receiver".to_string()),
|
||||||
Value::String(self.name.clone()),
|
Value::String(self.name.clone().to_string()),
|
||||||
);
|
);
|
||||||
route.insert(
|
route.insert(
|
||||||
Value::String("matchers".to_string()),
|
Value::String("matchers".to_string()),
|
||||||
@@ -284,7 +355,7 @@ impl DiscordWebhook {
|
|||||||
let mut receiver = Mapping::new();
|
let mut receiver = Mapping::new();
|
||||||
receiver.insert(
|
receiver.insert(
|
||||||
Value::String("name".to_string()),
|
Value::String("name".to_string()),
|
||||||
Value::String(self.name.clone()),
|
Value::String(self.name.clone().to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut discord_config = Mapping::new();
|
let mut discord_config = Mapping::new();
|
||||||
@@ -309,8 +380,9 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn discord_serialize_should_match() {
|
async fn discord_serialize_should_match() {
|
||||||
let discord_receiver = DiscordWebhook {
|
let discord_receiver = DiscordWebhook {
|
||||||
name: "test-discord".to_string(),
|
name: K8sName("test-discord".to_string()),
|
||||||
url: Url::Url(url::Url::parse("https://discord.i.dont.exist.com").unwrap()),
|
url: Url::Url(url::Url::parse("https://discord.i.dont.exist.com").unwrap()),
|
||||||
|
selectors: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let discord_receiver_receiver =
|
let discord_receiver_receiver =
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
prometheus::prometheus::{Prometheus, PrometheusReceiver},
|
prometheus::prometheus::{Prometheus, PrometheusReceiver},
|
||||||
},
|
},
|
||||||
topology::oberservability::monitoring::AlertReceiver,
|
topology::oberservability::monitoring::{AlertManagerReceiver, AlertReceiver},
|
||||||
};
|
};
|
||||||
use harmony_types::net::Url;
|
use harmony_types::net::Url;
|
||||||
|
|
||||||
@@ -31,6 +31,9 @@ pub struct WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<RHOBObservability> for WebhookReceiver {
|
impl AlertReceiver<RHOBObservability> for WebhookReceiver {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
|
||||||
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
|
||||||
data: json!({
|
data: json!({
|
||||||
@@ -97,6 +100,9 @@ impl AlertReceiver<RHOBObservability> for WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
|
impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
|
||||||
let spec = crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::AlertmanagerConfigSpec {
|
let spec = crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::AlertmanagerConfigSpec {
|
||||||
data: json!({
|
data: json!({
|
||||||
@@ -158,6 +164,9 @@ impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<Prometheus> for WebhookReceiver {
|
impl AlertReceiver<Prometheus> for WebhookReceiver {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
@@ -184,6 +193,9 @@ impl PrometheusReceiver for WebhookReceiver {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AlertReceiver<KubePrometheus> for WebhookReceiver {
|
impl AlertReceiver<KubePrometheus> for WebhookReceiver {
|
||||||
|
fn as_alertmanager_receiver(&self) -> Result<AlertManagerReceiver, String> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
|
||||||
sender.install_receiver(self).await
|
sender.install_receiver(self).await
|
||||||
}
|
}
|
||||||
|
|||||||
270
harmony/src/modules/monitoring/okd/cluster_monitoring.rs
Normal file
270
harmony/src/modules/monitoring/okd/cluster_monitoring.rs
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
use base64::prelude::*;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use harmony_types::id::Id;
|
||||||
|
use kube::api::DynamicObject;
|
||||||
|
use log::{debug, info, trace};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data::Version,
|
||||||
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
|
inventory::Inventory,
|
||||||
|
modules::monitoring::okd::OpenshiftClusterAlertSender,
|
||||||
|
score::Score,
|
||||||
|
topology::{K8sclient, Topology, oberservability::monitoring::AlertReceiver},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Clone for Box<dyn AlertReceiver<OpenshiftClusterAlertSender>> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
self.clone_box()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Box<dyn AlertReceiver<OpenshiftClusterAlertSender>> {
|
||||||
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct OpenshiftClusterAlertScore {
|
||||||
|
pub receivers: Vec<Box<dyn AlertReceiver<OpenshiftClusterAlertSender>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + K8sclient> Score<T> for OpenshiftClusterAlertScore {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"ClusterAlertScore".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(OpenshiftClusterAlertInterpret {
|
||||||
|
receivers: self.receivers.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OpenshiftClusterAlertInterpret {
|
||||||
|
receivers: Vec<Box<dyn AlertReceiver<OpenshiftClusterAlertSender>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + K8sclient> Interpret<T> for OpenshiftClusterAlertInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
_inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let client = topology.k8s_client().await?;
|
||||||
|
let openshift_monitoring_namespace = "openshift-monitoring";
|
||||||
|
|
||||||
|
let mut alertmanager_main_secret: DynamicObject = client
|
||||||
|
.get_secret_json_value("alertmanager-main", Some(openshift_monitoring_namespace))
|
||||||
|
.await?;
|
||||||
|
trace!("Got secret {alertmanager_main_secret:#?}");
|
||||||
|
|
||||||
|
let data: &mut serde_json::Value = &mut alertmanager_main_secret.data;
|
||||||
|
trace!("Alertmanager-main secret data {data:#?}");
|
||||||
|
let data_obj = data
|
||||||
|
.get_mut("data")
|
||||||
|
.ok_or(InterpretError::new(
|
||||||
|
"Missing 'data' field in alertmanager-main secret.".to_string(),
|
||||||
|
))?
|
||||||
|
.as_object_mut()
|
||||||
|
.ok_or(InterpretError::new(
|
||||||
|
"'data' field in alertmanager-main secret is expected to be an object ."
|
||||||
|
.to_string(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let config_b64 = data_obj
|
||||||
|
.get("alertmanager.yaml")
|
||||||
|
.ok_or(InterpretError::new(
|
||||||
|
"Missing 'alertmanager.yaml' in alertmanager-main secret data".to_string(),
|
||||||
|
))?
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("");
|
||||||
|
trace!("Config base64 {config_b64}");
|
||||||
|
|
||||||
|
let config_bytes = BASE64_STANDARD.decode(config_b64).unwrap_or_default();
|
||||||
|
|
||||||
|
let mut am_config: serde_yaml::Value =
|
||||||
|
serde_yaml::from_str(&String::from_utf8(config_bytes).unwrap_or_default())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
debug!("Current alertmanager config {am_config:#?}");
|
||||||
|
|
||||||
|
let existing_receivers_sequence = if let Some(receivers) = am_config.get_mut("receivers") {
|
||||||
|
match receivers.as_sequence_mut() {
|
||||||
|
Some(seq) => seq,
|
||||||
|
None => {
|
||||||
|
return Err(InterpretError::new(format!(
|
||||||
|
"Expected alertmanager config receivers to be a sequence, got {:?}",
|
||||||
|
receivers
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&mut serde_yaml::Sequence::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut additional_resources = vec![];
|
||||||
|
|
||||||
|
for custom_receiver in &self.receivers {
|
||||||
|
let name = custom_receiver.name();
|
||||||
|
let alertmanager_receiver = custom_receiver.as_alertmanager_receiver()?;
|
||||||
|
|
||||||
|
let receiver_json_value = alertmanager_receiver.receiver_config;
|
||||||
|
|
||||||
|
let receiver_yaml_string =
|
||||||
|
serde_json::to_string(&receiver_json_value).map_err(|e| {
|
||||||
|
InterpretError::new(format!("Failed to serialize receiver config: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let receiver_yaml_value: serde_yaml::Value =
|
||||||
|
serde_yaml::from_str(&receiver_yaml_string).map_err(|e| {
|
||||||
|
InterpretError::new(format!("Failed to parse receiver config as YAML: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(idx) = existing_receivers_sequence.iter().position(|r| {
|
||||||
|
r.get("name")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map_or(false, |n| n == name)
|
||||||
|
}) {
|
||||||
|
info!("Replacing existing AlertManager receiver: {}", name);
|
||||||
|
existing_receivers_sequence[idx] = receiver_yaml_value;
|
||||||
|
} else {
|
||||||
|
debug!("Adding new AlertManager receiver: {}", name);
|
||||||
|
existing_receivers_sequence.push(receiver_yaml_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
additional_resources.push(alertmanager_receiver.additional_ressources);
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_route_mapping = if let Some(route) = am_config.get_mut("route") {
|
||||||
|
match route.as_mapping_mut() {
|
||||||
|
Some(map) => map,
|
||||||
|
None => {
|
||||||
|
return Err(InterpretError::new(format!(
|
||||||
|
"Expected alertmanager config route to be a mapping, got {:?}",
|
||||||
|
route
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&mut serde_yaml::Mapping::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing_route_sequence = if let Some(routes) = existing_route_mapping.get_mut("routes")
|
||||||
|
{
|
||||||
|
match routes.as_sequence_mut() {
|
||||||
|
Some(seq) => seq,
|
||||||
|
None => {
|
||||||
|
return Err(InterpretError::new(format!(
|
||||||
|
"Expected alertmanager config routes to be a sequence, got {:?}",
|
||||||
|
routes
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&mut serde_yaml::Sequence::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for custom_receiver in &self.receivers {
|
||||||
|
let name = custom_receiver.name();
|
||||||
|
let alertmanager_receiver = custom_receiver.as_alertmanager_receiver()?;
|
||||||
|
|
||||||
|
let route_json_value = alertmanager_receiver.route_config;
|
||||||
|
let route_yaml_string = serde_json::to_string(&route_json_value).map_err(|e| {
|
||||||
|
InterpretError::new(format!("Failed to serialize route config: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let route_yaml_value: serde_yaml::Value = serde_yaml::from_str(&route_yaml_string)
|
||||||
|
.map_err(|e| {
|
||||||
|
InterpretError::new(format!("Failed to parse route config as YAML: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(idy) = existing_route_sequence.iter().position(|r| {
|
||||||
|
r.get("receiver")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map_or(false, |n| n == name)
|
||||||
|
}) {
|
||||||
|
info!("Replacing existing AlertManager receiver: {}", name);
|
||||||
|
existing_route_sequence[idy] = route_yaml_value;
|
||||||
|
} else {
|
||||||
|
debug!("Adding new AlertManager receiver: {}", name);
|
||||||
|
existing_route_sequence.push(route_yaml_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("Current alertmanager config {am_config:#?}");
|
||||||
|
// TODO
|
||||||
|
// - save new version of alertmanager config
|
||||||
|
// - write additional ressources to the cluster
|
||||||
|
let am_config = serde_yaml::to_string(&am_config).map_err(|e| {
|
||||||
|
InterpretError::new(format!(
|
||||||
|
"Failed to serialize new alertmanager config to string : {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut am_config_b64 = String::new();
|
||||||
|
BASE64_STANDARD.encode_string(am_config, &mut am_config_b64);
|
||||||
|
|
||||||
|
// TODO put update configmap value and save new value
|
||||||
|
data_obj.insert(
|
||||||
|
"alertmanager.yaml".to_string(),
|
||||||
|
serde_json::Value::String(am_config_b64),
|
||||||
|
);
|
||||||
|
|
||||||
|
// https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management
|
||||||
|
alertmanager_main_secret.metadata.managed_fields = None;
|
||||||
|
|
||||||
|
trace!("Applying new alertmanager_main_secret {alertmanager_main_secret:#?}");
|
||||||
|
client
|
||||||
|
.apply_dynamic(
|
||||||
|
&alertmanager_main_secret,
|
||||||
|
Some(openshift_monitoring_namespace),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let additional_resources = additional_resources.concat();
|
||||||
|
trace!("Applying additional ressources for alert receivers {additional_resources:#?}");
|
||||||
|
client
|
||||||
|
.apply_dynamic_many(
|
||||||
|
&additional_resources,
|
||||||
|
Some(openshift_monitoring_namespace),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(format!(
|
||||||
|
"Successfully configured {} cluster alert receivers: {}",
|
||||||
|
self.receivers.len(),
|
||||||
|
self.receivers
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.name())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
InterpretName::Custom("OpenshiftClusterAlertInterpret")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
90
harmony/src/modules/monitoring/okd/config.rs
Normal file
90
harmony/src/modules/monitoring/okd/config.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
interpret::{InterpretError, Outcome},
|
||||||
|
topology::k8s::K8sClient,
|
||||||
|
};
|
||||||
|
use k8s_openapi::api::core::v1::ConfigMap;
|
||||||
|
use kube::api::ObjectMeta;
|
||||||
|
|
||||||
|
pub(crate) struct Config;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub async fn create_cluster_monitoring_config_cm(
|
||||||
|
client: &Arc<K8sClient>,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let mut data = BTreeMap::new();
|
||||||
|
data.insert(
|
||||||
|
"config.yaml".to_string(),
|
||||||
|
r#"
|
||||||
|
enableUserWorkload: true
|
||||||
|
alertmanagerMain:
|
||||||
|
enableUserAlertmanagerConfig: true
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cm = ConfigMap {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("cluster-monitoring-config".to_string()),
|
||||||
|
namespace: Some("openshift-monitoring".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data: Some(data),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
client.apply(&cm, Some("openshift-monitoring")).await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(
|
||||||
|
"updated cluster-monitoring-config-map".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_user_workload_monitoring_config_cm(
|
||||||
|
client: &Arc<K8sClient>,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let mut data = BTreeMap::new();
|
||||||
|
data.insert(
|
||||||
|
"config.yaml".to_string(),
|
||||||
|
r#"
|
||||||
|
alertmanager:
|
||||||
|
enabled: true
|
||||||
|
enableAlertmanagerConfig: true
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let cm = ConfigMap {
|
||||||
|
metadata: ObjectMeta {
|
||||||
|
name: Some("user-workload-monitoring-config".to_string()),
|
||||||
|
namespace: Some("openshift-user-workload-monitoring".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data: Some(data),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
client
|
||||||
|
.apply(&cm, Some("openshift-user-workload-monitoring"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(
|
||||||
|
"updated openshift-user-monitoring-config-map".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn verify_user_workload(client: &Arc<K8sClient>) -> Result<Outcome, InterpretError> {
|
||||||
|
let namespace = "openshift-user-workload-monitoring";
|
||||||
|
let alertmanager_name = "alertmanager-user-workload-0";
|
||||||
|
let prometheus_name = "prometheus-user-workload-0";
|
||||||
|
client
|
||||||
|
.wait_for_pod_ready(alertmanager_name, Some(namespace))
|
||||||
|
.await?;
|
||||||
|
client
|
||||||
|
.wait_for_pod_ready(prometheus_name, Some(namespace))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(format!(
|
||||||
|
"pods: {}, {} ready in ns: {}",
|
||||||
|
alertmanager_name, prometheus_name, namespace
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
use std::{collections::BTreeMap, sync::Arc};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Version,
|
data::Version,
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
|
modules::monitoring::okd::config::Config,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{K8sclient, Topology, k8s::K8sClient},
|
topology::{K8sclient, Topology},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_types::id::Id;
|
use harmony_types::id::Id;
|
||||||
use k8s_openapi::api::core::v1::ConfigMap;
|
|
||||||
use kube::api::ObjectMeta;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
@@ -37,10 +34,9 @@ impl<T: Topology + K8sclient> Interpret<T> for OpenshiftUserWorkloadMonitoringIn
|
|||||||
topology: &T,
|
topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
let client = topology.k8s_client().await.unwrap();
|
let client = topology.k8s_client().await.unwrap();
|
||||||
self.update_cluster_monitoring_config_cm(&client).await?;
|
Config::create_cluster_monitoring_config_cm(&client).await?;
|
||||||
self.update_user_workload_monitoring_config_cm(&client)
|
Config::create_user_workload_monitoring_config_cm(&client).await?;
|
||||||
.await?;
|
Config::verify_user_workload(&client).await?;
|
||||||
self.verify_user_workload(&client).await?;
|
|
||||||
Ok(Outcome::success(
|
Ok(Outcome::success(
|
||||||
"successfully enabled user-workload-monitoring".to_string(),
|
"successfully enabled user-workload-monitoring".to_string(),
|
||||||
))
|
))
|
||||||
@@ -62,88 +58,3 @@ impl<T: Topology + K8sclient> Interpret<T> for OpenshiftUserWorkloadMonitoringIn
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenshiftUserWorkloadMonitoringInterpret {
|
|
||||||
pub async fn update_cluster_monitoring_config_cm(
|
|
||||||
&self,
|
|
||||||
client: &Arc<K8sClient>,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let mut data = BTreeMap::new();
|
|
||||||
data.insert(
|
|
||||||
"config.yaml".to_string(),
|
|
||||||
r#"
|
|
||||||
enableUserWorkload: true
|
|
||||||
alertmanagerMain:
|
|
||||||
enableUserAlertmanagerConfig: true
|
|
||||||
"#
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cm = ConfigMap {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("cluster-monitoring-config".to_string()),
|
|
||||||
namespace: Some("openshift-monitoring".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
data: Some(data),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
client.apply(&cm, Some("openshift-monitoring")).await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(
|
|
||||||
"updated cluster-monitoring-config-map".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_user_workload_monitoring_config_cm(
|
|
||||||
&self,
|
|
||||||
client: &Arc<K8sClient>,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let mut data = BTreeMap::new();
|
|
||||||
data.insert(
|
|
||||||
"config.yaml".to_string(),
|
|
||||||
r#"
|
|
||||||
alertmanager:
|
|
||||||
enabled: true
|
|
||||||
enableAlertmanagerConfig: true
|
|
||||||
"#
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
let cm = ConfigMap {
|
|
||||||
metadata: ObjectMeta {
|
|
||||||
name: Some("user-workload-monitoring-config".to_string()),
|
|
||||||
namespace: Some("openshift-user-workload-monitoring".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
data: Some(data),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.apply(&cm, Some("openshift-user-workload-monitoring"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(
|
|
||||||
"updated openshift-user-monitoring-config-map".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn verify_user_workload(
|
|
||||||
&self,
|
|
||||||
client: &Arc<K8sClient>,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let namespace = "openshift-user-workload-monitoring";
|
|
||||||
let alertmanager_name = "alertmanager-user-workload-0";
|
|
||||||
let prometheus_name = "prometheus-user-workload-0";
|
|
||||||
client
|
|
||||||
.wait_for_pod_ready(alertmanager_name, Some(namespace))
|
|
||||||
.await?;
|
|
||||||
client
|
|
||||||
.wait_for_pod_ready(prometheus_name, Some(namespace))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"pods: {}, {} ready in ns: {}",
|
|
||||||
alertmanager_name, prometheus_name, namespace
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1,14 @@
|
|||||||
|
use crate::topology::oberservability::monitoring::AlertSender;
|
||||||
|
|
||||||
|
pub mod cluster_monitoring;
|
||||||
|
pub(crate) mod config;
|
||||||
pub mod enable_user_workload;
|
pub mod enable_user_workload;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OpenshiftClusterAlertSender;
|
||||||
|
|
||||||
|
impl AlertSender for OpenshiftClusterAlertSender {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"OpenshiftClusterAlertSender".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
96
harmony_types/src/k8s_name.rs
Normal file
96
harmony_types/src/k8s_name.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
|
pub struct K8sName(pub String);
|
||||||
|
|
||||||
|
impl K8sName {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn dummy() -> Self {
|
||||||
|
K8sName("example".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid(name: &str) -> bool {
|
||||||
|
if name.is_empty() || name.len() > 63 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = name.as_bytes();
|
||||||
|
|
||||||
|
if !b[0].is_ascii_alphanumeric() || !b[b.len() - 1].is_ascii_alphanumeric() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
b.iter()
|
||||||
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for K8sName {
|
||||||
|
type Err = K8sNameError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if !Self::is_valid(s) {
|
||||||
|
return Err(K8sNameError::InvalidFormat(format!(
|
||||||
|
"Invalid Kubernetes resource name '{s}': \
|
||||||
|
must match DNS-1123 (lowercase alphanumeric, hyphens, <=63 chars)"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(K8sName(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum K8sNameError {
|
||||||
|
InvalidFormat(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&K8sName> for String {
|
||||||
|
fn from(value: &K8sName) -> Self {
|
||||||
|
value.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for K8sName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_name() {
|
||||||
|
assert!(K8sName::from_str("k8s-name-test").is_ok());
|
||||||
|
assert!(K8sName::from_str("n").is_ok());
|
||||||
|
assert!(K8sName::from_str("node1").is_ok());
|
||||||
|
assert!(K8sName::from_str("my-app-v2").is_ok());
|
||||||
|
assert!(K8sName::from_str("service123").is_ok());
|
||||||
|
assert!(K8sName::from_str("abcdefghijklmnopqrstuvwxyz-1234567890").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_name() {
|
||||||
|
assert!(K8sName::from_str("").is_err());
|
||||||
|
assert!(K8sName::from_str(".config").is_err());
|
||||||
|
assert!(K8sName::from_str("_hidden").is_err());
|
||||||
|
assert!(K8sName::from_str("UPPER-CASE").is_err());
|
||||||
|
assert!(K8sName::from_str("123-$$$").is_err());
|
||||||
|
assert!(K8sName::from_str("app!name").is_err());
|
||||||
|
assert!(K8sName::from_str("my..app").is_err());
|
||||||
|
assert!(K8sName::from_str("backend-").is_err());
|
||||||
|
assert!(K8sName::from_str("-frontend").is_err());
|
||||||
|
assert!(K8sName::from_str("InvalidName").is_err());
|
||||||
|
assert!(
|
||||||
|
K8sName::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(K8sName::from_str("k8s name").is_err());
|
||||||
|
assert!(K8sName::from_str("k8s_name").is_err());
|
||||||
|
assert!(K8sName::from_str("k8s@name").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod id;
|
pub mod id;
|
||||||
|
pub mod k8s_name;
|
||||||
pub mod net;
|
pub mod net;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
|
|||||||
Reference in New Issue
Block a user