Compare commits

..

1 Commits

Author SHA1 Message Date
d4f3b32c3b feat: impl clone_box for ScrapeTarget<CRDPrometheus>
Some checks failed
Run Check Script / check (pull_request) Failing after 34s
2025-10-24 12:04:19 -04:00
53 changed files with 379 additions and 1752 deletions

19
Cargo.lock generated
View File

@@ -1804,25 +1804,6 @@ 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"

View File

@@ -31,7 +31,6 @@ pub struct BrocadeOptions {
pub struct TimeoutConfig { pub struct TimeoutConfig {
pub shell_ready: Duration, pub shell_ready: Duration,
pub command_execution: Duration, pub command_execution: Duration,
pub command_output: Duration,
pub cleanup: Duration, pub cleanup: Duration,
pub message_wait: Duration, pub message_wait: Duration,
} }
@@ -41,7 +40,6 @@ impl Default for TimeoutConfig {
Self { Self {
shell_ready: Duration::from_secs(10), shell_ready: Duration::from_secs(10),
command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while command_execution: Duration::from_secs(60), // Commands like `deploy` (for a LAG) can take a while
command_output: Duration::from_secs(5), // Delay to start logging "waiting for command output"
cleanup: Duration::from_secs(10), cleanup: Duration::from_secs(10),
message_wait: Duration::from_millis(500), message_wait: Duration::from_millis(500),
} }

View File

@@ -3,7 +3,6 @@ use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::switch::{PortDeclaration, PortLocation}; use harmony_types::switch::{PortDeclaration, PortLocation};
use log::{debug, info}; use log::{debug, info};
use regex::Regex;
use crate::{ use crate::{
BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo, BrocadeClient, BrocadeInfo, Error, ExecutionMode, InterSwitchLink, InterfaceInfo,
@@ -104,37 +103,13 @@ impl NetworkOperatingSystemClient {
}; };
Some(Ok(InterfaceInfo { Some(Ok(InterfaceInfo {
name: format!("{interface_type} {port_location}"), name: format!("{} {}", interface_type, port_location),
port_location, port_location,
interface_type, interface_type,
operating_mode, operating_mode,
status, status,
})) }))
} }
fn map_configure_interfaces_error(&self, err: Error) -> Error {
debug!("[Brocade] {err}");
if let Error::CommandError(message) = &err {
if message.contains("switchport")
&& message.contains("Cannot configure aggregator member")
{
let re = Regex::new(r"\(conf-if-([a-zA-Z]+)-([\d/]+)\)#").unwrap();
if let Some(caps) = re.captures(message) {
let interface_type = &caps[1];
let port_location = &caps[2];
let interface = format!("{interface_type} {port_location}");
return Error::CommandError(format!(
"Cannot configure interface '{interface}', it is a member of a port-channel (LAG)"
));
}
}
}
err
}
} }
#[async_trait] #[async_trait]
@@ -222,10 +197,11 @@ impl BrocadeClient for NetworkOperatingSystemClient {
commands.push("exit".into()); commands.push("exit".into());
} }
commands.push("write memory".into());
self.shell self.shell
.run_commands(commands, ExecutionMode::Regular) .run_commands(commands, ExecutionMode::Regular)
.await .await?;
.map_err(|err| self.map_configure_interfaces_error(err))?;
info!("[Brocade] Interfaces configured."); info!("[Brocade] Interfaces configured.");
@@ -237,7 +213,7 @@ impl BrocadeClient for NetworkOperatingSystemClient {
let output = self let output = self
.shell .shell
.run_command("show port-channel summary", ExecutionMode::Regular) .run_command("show port-channel", ExecutionMode::Regular)
.await?; .await?;
let used_ids: Vec<u8> = output let used_ids: Vec<u8> = output
@@ -272,12 +248,7 @@ impl BrocadeClient for NetworkOperatingSystemClient {
ports: &[PortLocation], ports: &[PortLocation],
) -> Result<(), Error> { ) -> Result<(), Error> {
info!( info!(
"[Brocade] Configuring port-channel '{channel_id} {channel_name}' with ports: {}", "[Brocade] Configuring port-channel '{channel_name} {channel_id}' with ports: {ports:?}"
ports
.iter()
.map(|p| format!("{p}"))
.collect::<Vec<String>>()
.join(", ")
); );
let interfaces = self.get_interfaces().await?; let interfaces = self.get_interfaces().await?;
@@ -305,6 +276,8 @@ impl BrocadeClient for NetworkOperatingSystemClient {
commands.push("exit".into()); commands.push("exit".into());
} }
commands.push("write memory".into());
self.shell self.shell
.run_commands(commands, ExecutionMode::Regular) .run_commands(commands, ExecutionMode::Regular)
.await?; .await?;
@@ -321,6 +294,7 @@ impl BrocadeClient for NetworkOperatingSystemClient {
"configure terminal".into(), "configure terminal".into(),
format!("no interface port-channel {}", channel_name), format!("no interface port-channel {}", channel_name),
"exit".into(), "exit".into(),
"write memory".into(),
]; ];
self.shell self.shell

View File

@@ -211,7 +211,7 @@ impl BrocadeSession {
let mut output = Vec::new(); let mut output = Vec::new();
let start = Instant::now(); let start = Instant::now();
let read_timeout = Duration::from_millis(500); let read_timeout = Duration::from_millis(500);
let log_interval = Duration::from_secs(5); let log_interval = Duration::from_secs(3);
let mut last_log = Instant::now(); let mut last_log = Instant::now();
loop { loop {
@@ -221,9 +221,7 @@ impl BrocadeSession {
)); ));
} }
if start.elapsed() > self.options.timeouts.command_output if start.elapsed() > Duration::from_secs(5) && last_log.elapsed() > log_interval {
&& last_log.elapsed() > log_interval
{
info!("[Brocade] Waiting for command output..."); info!("[Brocade] Waiting for command output...");
last_log = Instant::now(); last_log = Instant::now();
} }
@@ -278,7 +276,7 @@ impl BrocadeSession {
let output_lower = output.to_lowercase(); let output_lower = output.to_lowercase();
if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) { if ERROR_PATTERNS.iter().any(|&p| output_lower.contains(p)) {
return Err(Error::CommandError(format!( return Err(Error::CommandError(format!(
"Command error: {}", "Command '{command}' failed: {}",
output.trim() output.trim()
))); )));
} }

View File

@@ -24,14 +24,13 @@ use harmony::{
}, },
topology::K8sAnywhereTopology, topology::K8sAnywhereTopology,
}; };
use harmony_types::{k8s_name::K8sName, net::Url}; use harmony_types::net::Url;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let discord_receiver = DiscordWebhook { let discord_receiver = DiscordWebhook {
name: K8sName("test-discord".to_string()), name: "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();

View File

@@ -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,9 +43,8 @@ async fn main() {
}; };
let discord_receiver = DiscordWebhook { let discord_receiver = DiscordWebhook {
name: K8sName("test-discord".to_string()), name: "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();

View File

@@ -61,7 +61,6 @@ async fn main() {
let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1); let gateway_ipv4 = Ipv4Addr::new(192, 168, 33, 1);
let gateway_ip = IpAddr::V4(gateway_ipv4); let gateway_ip = IpAddr::V4(gateway_ipv4);
let topology = harmony::topology::HAClusterTopology { let topology = harmony::topology::HAClusterTopology {
kubeconfig: None,
domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly domain_name: "ncd0.harmony.mcd".to_string(), // TODO this must be set manually correctly
// when setting up the opnsense firewall // when setting up the opnsense firewall
router: Arc::new(UnmanagedRouter::new( router: Arc::new(UnmanagedRouter::new(

View File

@@ -1,22 +0,0 @@
[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" }

View File

@@ -1,38 +0,0 @@
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();
}

View File

@@ -59,7 +59,6 @@ pub async fn get_topology() -> HAClusterTopology {
let gateway_ipv4 = ipv4!("192.168.1.1"); let gateway_ipv4 = ipv4!("192.168.1.1");
let gateway_ip = IpAddr::V4(gateway_ipv4); let gateway_ip = IpAddr::V4(gateway_ipv4);
harmony::topology::HAClusterTopology { harmony::topology::HAClusterTopology {
kubeconfig: None,
domain_name: "demo.harmony.mcd".to_string(), domain_name: "demo.harmony.mcd".to_string(),
router: Arc::new(UnmanagedRouter::new( router: Arc::new(UnmanagedRouter::new(
gateway_ip, gateway_ip,

View File

@@ -54,7 +54,6 @@ pub async fn get_topology() -> HAClusterTopology {
let gateway_ipv4 = ipv4!("192.168.1.1"); let gateway_ipv4 = ipv4!("192.168.1.1");
let gateway_ip = IpAddr::V4(gateway_ipv4); let gateway_ip = IpAddr::V4(gateway_ipv4);
harmony::topology::HAClusterTopology { harmony::topology::HAClusterTopology {
kubeconfig: None,
domain_name: "demo.harmony.mcd".to_string(), domain_name: "demo.harmony.mcd".to_string(),
router: Arc::new(UnmanagedRouter::new( router: Arc::new(UnmanagedRouter::new(
gateway_ip, gateway_ip,

View File

@@ -57,7 +57,6 @@ async fn main() {
let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1); let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1);
let gateway_ip = IpAddr::V4(gateway_ipv4); let gateway_ip = IpAddr::V4(gateway_ipv4);
let topology = harmony::topology::HAClusterTopology { let topology = harmony::topology::HAClusterTopology {
kubeconfig: None,
domain_name: "demo.harmony.mcd".to_string(), domain_name: "demo.harmony.mcd".to_string(),
router: Arc::new(UnmanagedRouter::new( router: Arc::new(UnmanagedRouter::new(
gateway_ip, gateway_ip,

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf, sync::Arc}; use std::{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::{k8s_name::K8sName, net::Url}; use harmony_types::net::Url;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -22,9 +22,8 @@ async fn main() {
}); });
let discord_receiver = DiscordWebhook { let discord_receiver = DiscordWebhook {
name: K8sName("test-discord".to_string()), name: "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 {

View File

@@ -1,4 +1,3 @@
Dockerfile.harmony Dockerfile.harmony
.harmony_generated .harmony_generated
harmony harmony
webapp

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
@@ -14,7 +14,6 @@ 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() {
@@ -26,9 +25,8 @@ async fn main() {
}); });
let discord_receiver = DiscordWebhook { let discord_receiver = DiscordWebhook {
name: K8sName("test-discord".to_string()), name: "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 {

View File

@@ -1,7 +0,0 @@
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"

View File

@@ -1,16 +0,0 @@
{{/*
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 }}

View File

@@ -1,23 +0,0 @@
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

View File

@@ -1,35 +0,0 @@
{{- 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 }}

View File

@@ -1,14 +0,0 @@
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" . }}

View File

@@ -1,34 +0,0 @@
# 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

View File

@@ -10,7 +10,6 @@ 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]
@@ -32,9 +31,8 @@ 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: K8sName("test-discord".to_string()), name: "test-discord".to_string(),
url: hurl!("https://discord.doesnt.exist.com"), url: hurl!("https://discord.doesnt.exist.com"),
selectors: vec![],
})], })],
}), }),
], ],

View File

@@ -4,16 +4,19 @@ use harmony_types::{
net::{MacAddress, Url}, net::{MacAddress, Url},
switch::PortLocation, switch::PortLocation,
}; };
use k8s_openapi::api::core::v1::Namespace;
use kube::api::ObjectMeta; use kube::api::ObjectMeta;
use log::debug; use log::debug;
use log::info; use log::info;
use crate::modules::okd::crd::nmstate::{self, NodeNetworkConfigurationPolicy}; use crate::data::FileContent;
use crate::topology::PxeOptions; use crate::executors::ExecutorError;
use crate::{data::FileContent, modules::okd::crd::nmstate::NMState}; use crate::hardware::PhysicalHost;
use crate::{ use crate::modules::okd::crd::{
executors::ExecutorError, modules::okd::crd::nmstate::NodeNetworkConfigurationPolicySpec, InstallPlanApproval, OperatorGroup, OperatorGroupSpec, Subscription, SubscriptionSpec,
nmstate::{self, NMState, NodeNetworkConfigurationPolicy, NodeNetworkConfigurationPolicySpec},
}; };
use crate::topology::PxeOptions;
use super::{ use super::{
DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig, DHCPStaticEntry, DhcpServer, DnsRecord, DnsRecordType, DnsServer, Firewall, HostNetworkConfig,
@@ -39,7 +42,6 @@ pub struct HAClusterTopology {
pub bootstrap_host: LogicalHost, pub bootstrap_host: LogicalHost,
pub control_plane: Vec<LogicalHost>, pub control_plane: Vec<LogicalHost>,
pub workers: Vec<LogicalHost>, pub workers: Vec<LogicalHost>,
pub kubeconfig: Option<String>,
} }
#[async_trait] #[async_trait]
@@ -58,17 +60,9 @@ impl Topology for HAClusterTopology {
#[async_trait] #[async_trait]
impl K8sclient for HAClusterTopology { impl K8sclient for HAClusterTopology {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> { async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
match &self.kubeconfig { Ok(Arc::new(
None => Ok(Arc::new(
K8sClient::try_default().await.map_err(|e| e.to_string())?, K8sClient::try_default().await.map_err(|e| e.to_string())?,
)), ))
Some(kubeconfig) => {
let Some(client) = K8sClient::from_kubeconfig(&kubeconfig).await else {
return Err("Failed to create k8s client".to_string());
};
Ok(Arc::new(client))
}
}
} }
} }
@@ -94,47 +88,59 @@ impl HAClusterTopology {
} }
async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> { async fn ensure_nmstate_operator_installed(&self) -> Result<(), String> {
// FIXME: Find a way to check nmstate is already available (get pod -n openshift-nmstate)
debug!("Installing NMState operator...");
let k8s_client = self.k8s_client().await?; let k8s_client = self.k8s_client().await?;
debug!("Installing NMState controller..."); let nmstate_namespace = Namespace {
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/nmstate.io_nmstates.yaml metadata: ObjectMeta {
").unwrap(), Some("nmstate")) name: Some("openshift-nmstate".to_string()),
.await finalizers: Some(vec!["kubernetes".to_string()]),
.map_err(|e| e.to_string())?; ..Default::default()
},
debug!("Creating NMState namespace..."); ..Default::default()
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/namespace.yaml };
").unwrap(), Some("nmstate")) debug!("Creating NMState namespace: {nmstate_namespace:#?}");
.await
.map_err(|e| e.to_string())?;
debug!("Creating NMState service account...");
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/service_account.yaml
").unwrap(), Some("nmstate"))
.await
.map_err(|e| e.to_string())?;
debug!("Creating NMState role...");
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/role.yaml
").unwrap(), Some("nmstate"))
.await
.map_err(|e| e.to_string())?;
debug!("Creating NMState role binding...");
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/role_binding.yaml
").unwrap(), Some("nmstate"))
.await
.map_err(|e| e.to_string())?;
debug!("Creating NMState operator...");
k8s_client.apply_url(url::Url::parse("https://github.com/nmstate/kubernetes-nmstate/releases/download/v0.84.0/operator.yaml
").unwrap(), Some("nmstate"))
.await
.map_err(|e| e.to_string())?;
k8s_client k8s_client
.wait_until_deployment_ready("nmstate-operator", Some("nmstate"), None) .apply(&nmstate_namespace, None)
.await?; .await
.map_err(|e| e.to_string())?;
let nmstate_operator_group = OperatorGroup {
metadata: ObjectMeta {
name: Some("openshift-nmstate".to_string()),
namespace: Some("openshift-nmstate".to_string()),
..Default::default()
},
spec: OperatorGroupSpec {
target_namespaces: vec!["openshift-nmstate".to_string()],
},
};
debug!("Creating NMState operator group: {nmstate_operator_group:#?}");
k8s_client
.apply(&nmstate_operator_group, None)
.await
.map_err(|e| e.to_string())?;
let nmstate_subscription = Subscription {
metadata: ObjectMeta {
name: Some("kubernetes-nmstate-operator".to_string()),
namespace: Some("openshift-nmstate".to_string()),
..Default::default()
},
spec: SubscriptionSpec {
channel: Some("stable".to_string()),
install_plan_approval: Some(InstallPlanApproval::Automatic),
name: "kubernetes-nmstate-operator".to_string(),
source: "redhat-operators".to_string(),
source_namespace: "openshift-marketplace".to_string(),
},
};
debug!("Subscribing to NMState Operator: {nmstate_subscription:#?}");
k8s_client
.apply(&nmstate_subscription, None)
.await
.map_err(|e| e.to_string())?;
let nmstate = NMState { let nmstate = NMState {
metadata: ObjectMeta { metadata: ObjectMeta {
@@ -156,7 +162,11 @@ impl HAClusterTopology {
42 // FIXME: Find a better way to declare the bond id 42 // FIXME: Find a better way to declare the bond id
} }
async fn configure_bond(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { async fn configure_bond(
&self,
host: &PhysicalHost,
config: &HostNetworkConfig,
) -> Result<(), SwitchError> {
self.ensure_nmstate_operator_installed() self.ensure_nmstate_operator_installed()
.await .await
.map_err(|e| { .map_err(|e| {
@@ -165,33 +175,29 @@ impl HAClusterTopology {
)) ))
})?; })?;
let bond_config = self.create_bond_configuration(config); let bond_config = self.create_bond_configuration(host, config);
debug!( debug!("Configuring bond for host {host:?}: {bond_config:#?}");
"Applying NMState bond config for host {}: {bond_config:#?}",
config.host_id
);
self.k8s_client() self.k8s_client()
.await .await
.unwrap() .unwrap()
.apply(&bond_config, None) .apply(&bond_config, None)
.await .await
.map_err(|e| SwitchError::new(format!("Failed to configure bond: {e}")))?; .unwrap();
Ok(()) todo!()
} }
fn create_bond_configuration( fn create_bond_configuration(
&self, &self,
host: &PhysicalHost,
config: &HostNetworkConfig, config: &HostNetworkConfig,
) -> NodeNetworkConfigurationPolicy { ) -> NodeNetworkConfigurationPolicy {
let host_name = &config.host_id; let host_name = host.id.clone();
let bond_id = self.get_next_bond_id(); let bond_id = self.get_next_bond_id();
let bond_name = format!("bond{bond_id}"); let bond_name = format!("bond{bond_id}");
info!("Configuring bond '{bond_name}' for host '{host_name}'...");
let mut bond_mtu: Option<u32> = None; let mut bond_mtu: Option<u32> = None;
let mut copy_mac_from: Option<String> = None; let mut bond_mac_address: Option<String> = None;
let mut bond_ports = Vec::new(); let mut bond_ports = Vec::new();
let mut interfaces: Vec<nmstate::InterfaceSpec> = Vec::new(); let mut interfaces: Vec<nmstate::InterfaceSpec> = Vec::new();
@@ -217,14 +223,14 @@ impl HAClusterTopology {
..Default::default() ..Default::default()
}); });
bond_ports.push(interface_name.clone()); bond_ports.push(interface_name);
// Use the first port's details for the bond mtu and mac address // Use the first port's details for the bond mtu and mac address
if bond_mtu.is_none() { if bond_mtu.is_none() {
bond_mtu = Some(switch_port.interface.mtu); bond_mtu = Some(switch_port.interface.mtu);
} }
if copy_mac_from.is_none() { if bond_mac_address.is_none() {
copy_mac_from = Some(interface_name); bond_mac_address = Some(switch_port.interface.mac_address.to_string());
} }
} }
@@ -233,7 +239,8 @@ impl HAClusterTopology {
description: Some(format!("Network bond for host {host_name}")), description: Some(format!("Network bond for host {host_name}")),
r#type: "bond".to_string(), r#type: "bond".to_string(),
state: "up".to_string(), state: "up".to_string(),
copy_mac_from, mtu: bond_mtu,
mac_address: bond_mac_address,
ipv4: Some(nmstate::IpStackSpec { ipv4: Some(nmstate::IpStackSpec {
dhcp: Some(true), dhcp: Some(true),
enabled: Some(true), enabled: Some(true),
@@ -268,12 +275,16 @@ impl HAClusterTopology {
} }
} }
async fn configure_port_channel(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { async fn configure_port_channel(
&self,
host: &PhysicalHost,
config: &HostNetworkConfig,
) -> Result<(), SwitchError> {
debug!("Configuring port channel: {config:#?}"); debug!("Configuring port channel: {config:#?}");
let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect(); let switch_ports = config.switch_ports.iter().map(|s| s.port.clone()).collect();
self.switch_client self.switch_client
.configure_port_channel(&format!("Harmony_{}", config.host_id), switch_ports) .configure_port_channel(&format!("Harmony_{}", host.id), switch_ports)
.await .await
.map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?; .map_err(|e| SwitchError::new(format!("Failed to configure switch: {e}")))?;
@@ -288,7 +299,6 @@ impl HAClusterTopology {
}; };
Self { Self {
kubeconfig: None,
domain_name: "DummyTopology".to_string(), domain_name: "DummyTopology".to_string(),
router: dummy_infra.clone(), router: dummy_infra.clone(),
load_balancer: dummy_infra.clone(), load_balancer: dummy_infra.clone(),
@@ -470,9 +480,13 @@ impl Switch for HAClusterTopology {
Ok(port) Ok(port)
} }
async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError> { async fn configure_host_network(
self.configure_bond(config).await?; &self,
self.configure_port_channel(config).await host: &PhysicalHost,
config: HostNetworkConfig,
) -> Result<(), SwitchError> {
self.configure_bond(host, &config).await?;
self.configure_port_channel(host, &config).await
} }
} }

View File

@@ -13,8 +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::{DynamicResourceScope, ErrorResponse}, core::ErrorResponse,
discovery::{ApiCapabilities, Scope},
error::DiscoveryError, error::DiscoveryError,
runtime::reflector::Lookup, runtime::reflector::Lookup,
}; };
@@ -23,12 +22,11 @@ use kube::{
api::{ApiResource, GroupVersionKind}, api::{ApiResource, GroupVersionKind},
runtime::wait::await_condition, runtime::wait::await_condition,
}; };
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace};
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use serde_json::json; use serde_json::json;
use similar::TextDiff; use similar::TextDiff;
use tokio::{io::AsyncReadExt, time::sleep}; use tokio::{io::AsyncReadExt, time::sleep};
use url::Url;
#[derive(new, Clone)] #[derive(new, Clone)]
pub struct K8sClient { pub struct K8sClient {
@@ -90,25 +88,7 @@ impl K8sClient {
} else { } else {
Api::default_namespaced_with(self.client.clone(), &gvk) Api::default_namespaced_with(self.client.clone(), &gvk)
}; };
Ok(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(
@@ -123,9 +103,8 @@ impl K8sClient {
debug!("getting default namespace deployment"); debug!("getting default namespace deployment");
Api::default_namespaced(self.client.clone()) Api::default_namespaced(self.client.clone())
}; };
debug!("getting deployment {} in ns {}", name, namespace.unwrap()); debug!("getting deployment {} in ns {}", name, namespace.unwrap());
deps.get_opt(name).await Ok(deps.get_opt(name).await?)
} }
pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> { pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> {
@@ -134,8 +113,7 @@ impl K8sClient {
} else { } else {
Api::default_namespaced(self.client.clone()) Api::default_namespaced(self.client.clone())
}; };
Ok(pods.get_opt(name).await?)
pods.get_opt(name).await
} }
pub async fn scale_deployment( pub async fn scale_deployment(
@@ -178,9 +156,9 @@ impl K8sClient {
pub async fn wait_until_deployment_ready( pub async fn wait_until_deployment_ready(
&self, &self,
name: &str, name: String,
namespace: Option<&str>, namespace: Option<&str>,
timeout: Option<Duration>, timeout: Option<u64>,
) -> Result<(), String> { ) -> Result<(), String> {
let api: Api<Deployment>; let api: Api<Deployment>;
@@ -190,9 +168,9 @@ impl K8sClient {
api = Api::default_namespaced(self.client.clone()); api = Api::default_namespaced(self.client.clone());
} }
let establish = await_condition(api, name, conditions::is_deployment_completed()); let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
let timeout = timeout.unwrap_or(Duration::from_secs(120)); let t = timeout.unwrap_or(300);
let res = tokio::time::timeout(timeout, establish).await; let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await;
if res.is_ok() { if res.is_ok() {
Ok(()) Ok(())
@@ -282,7 +260,7 @@ impl K8sClient {
if let Some(s) = status.status { if let Some(s) = status.status {
let mut stdout_buf = String::new(); let mut stdout_buf = String::new();
if let Some(mut stdout) = process.stdout() { if let Some(mut stdout) = process.stdout().take() {
stdout stdout
.read_to_string(&mut stdout_buf) .read_to_string(&mut stdout_buf)
.await .await
@@ -354,169 +332,6 @@ 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(&current).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(&current_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(&current_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
@@ -551,14 +366,14 @@ impl K8sClient {
Ok(current) => { Ok(current) => {
trace!("Received current value {current:#?}"); trace!("Received current value {current:#?}");
// The resource exists, so we calculate and display a diff. // The resource exists, so we calculate and display a diff.
println!("\nPerforming dry-run for resource: '{name}'"); println!("\nPerforming dry-run for resource: '{}'", name);
let mut current_yaml = serde_yaml::to_value(&current).unwrap_or_else(|_| { let mut current_yaml = serde_yaml::to_value(&current).unwrap_or_else(|_| {
panic!("Could not serialize current value : {current:#?}") panic!("Could not serialize current value : {current:#?}")
}); });
if current_yaml.is_mapping() && current_yaml.get("status").is_some() { if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
let map = current_yaml.as_mapping_mut().unwrap(); let map = current_yaml.as_mapping_mut().unwrap();
let removed = map.remove_entry("status"); let removed = map.remove_entry("status");
trace!("Removed status {removed:?}"); trace!("Removed status {:?}", removed);
} else { } else {
trace!( trace!(
"Did not find status entry for current object {}/{}", "Did not find status entry for current object {}/{}",
@@ -587,14 +402,14 @@ impl K8sClient {
similar::ChangeTag::Insert => "+", similar::ChangeTag::Insert => "+",
similar::ChangeTag::Equal => " ", similar::ChangeTag::Equal => " ",
}; };
print!("{sign}{change}"); print!("{}{}", sign, change);
} }
// In a dry run, we return the new resource state that would have been applied. // In a dry run, we return the new resource state that would have been applied.
Ok(resource.clone()) Ok(resource.clone())
} }
Err(Error::Api(ErrorResponse { code: 404, .. })) => { Err(Error::Api(ErrorResponse { code: 404, .. })) => {
// The resource does not exist, so the "diff" is the entire new resource. // The resource does not exist, so the "diff" is the entire new resource.
println!("\nPerforming dry-run for new resource: '{name}'"); println!("\nPerforming dry-run for new resource: '{}'", name);
println!( println!(
"Resource does not exist. It would be created with the following content:" "Resource does not exist. It would be created with the following content:"
); );
@@ -603,14 +418,14 @@ impl K8sClient {
// Print each line of the new resource with a '+' prefix. // Print each line of the new resource with a '+' prefix.
for line in new_yaml.lines() { for line in new_yaml.lines() {
println!("+{line}"); println!("+{}", line);
} }
// In a dry run, we return the new resource state that would have been created. // In a dry run, we return the new resource state that would have been created.
Ok(resource.clone()) Ok(resource.clone())
} }
Err(e) => { Err(e) => {
// Another API error occurred. // Another API error occurred.
error!("Failed to get resource '{name}': {e}"); error!("Failed to get resource '{}': {}", name, e);
Err(e) Err(e)
} }
} }
@@ -625,7 +440,7 @@ impl K8sClient {
where where
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
<K as Resource>::Scope: ApplyStrategy<K>, <K as Resource>::Scope: ApplyStrategy<K>,
<K as Resource>::DynamicType: Default, <K as kube::Resource>::DynamicType: Default,
{ {
let mut result = Vec::new(); let mut result = Vec::new();
for r in resource.iter() { for r in resource.iter() {
@@ -690,7 +505,10 @@ impl K8sClient {
// 6. Apply the object to the cluster using Server-Side Apply. // 6. Apply the object to the cluster using Server-Side Apply.
// This will create the resource if it doesn't exist, or update it if it does. // This will create the resource if it doesn't exist, or update it if it does.
println!("Applying '{name}' in namespace '{namespace}'...",); println!(
"Applying Argo Application '{}' in namespace '{}'...",
name, namespace
);
let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name
let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?; let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?;
@@ -699,51 +517,6 @@ impl K8sClient {
Ok(()) Ok(())
} }
/// Apply a resource from a URL
///
/// It is the equivalent of `kubectl apply -f <url>`
pub async fn apply_url(&self, url: Url, ns: Option<&str>) -> Result<(), Error> {
let patch_params = PatchParams::apply("harmony");
let discovery = kube::Discovery::new(self.client.clone()).run().await?;
let yaml = reqwest::get(url)
.await
.expect("Could not get URL")
.text()
.await
.expect("Could not get content from URL");
for doc in multidoc_deserialize(&yaml).expect("failed to parse YAML from file") {
let obj: DynamicObject =
serde_yaml::from_value(doc).expect("cannot apply without valid YAML");
let namespace = obj.metadata.namespace.as_deref().or(ns);
let type_meta = obj
.types
.as_ref()
.expect("cannot apply object without valid TypeMeta");
let gvk = GroupVersionKind::try_from(type_meta)
.expect("cannot apply object without valid GroupVersionKind");
let name = obj.name_any();
if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) {
let api = get_dynamic_api(ar, caps, self.client.clone(), namespace, false);
trace!(
"Applying {}: \n{}",
gvk.kind,
serde_yaml::to_string(&obj).expect("Failed to serialize YAML")
);
let data: serde_json::Value =
serde_json::to_value(&obj).expect("Failed to serialize JSON");
let _r = api.patch(&name, &patch_params, &Patch::Apply(data)).await?;
debug!("applied {} {}", gvk.kind, name);
} else {
warn!("Cannot apply document for unknown {gvk:?}");
}
}
Ok(())
}
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> { pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
let k = match Kubeconfig::read_from(path) { let k = match Kubeconfig::read_from(path) {
Ok(k) => k, Ok(k) => k,
@@ -763,31 +536,6 @@ impl K8sClient {
} }
} }
fn get_dynamic_api(
resource: ApiResource,
capabilities: ApiCapabilities,
client: Client,
ns: Option<&str>,
all: bool,
) -> Api<DynamicObject> {
if capabilities.scope == Scope::Cluster || all {
Api::all_with(client, &resource)
} else if let Some(namespace) = ns {
Api::namespaced_with(client, namespace, &resource)
} else {
Api::default_namespaced_with(client, &resource)
}
}
fn multidoc_deserialize(data: &str) -> Result<Vec<serde_yaml::Value>, serde_yaml::Error> {
use serde::Deserialize;
let mut docs = vec![];
for de in serde_yaml::Deserializer::from_str(data) {
docs.push(serde_yaml::Value::deserialize(de)?);
}
Ok(docs)
}
pub trait ApplyStrategy<K: Resource> { pub trait ApplyStrategy<K: Resource> {
fn get_api(client: &Client, ns: Option<&str>) -> Api<K>; fn get_api(client: &Client, ns: Option<&str>) -> Api<K>;
} }

View File

@@ -1,4 +1,4 @@
use std::{collections::BTreeMap, process::Command, sync::Arc, time::Duration}; use std::{collections::BTreeMap, process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{Engine, engine::general_purpose}; use base64::{Engine, engine::general_purpose};
@@ -155,9 +155,9 @@ impl Grafana for K8sAnywhereTopology {
//TODO change this to a ensure ready or something better than just a timeout //TODO change this to a ensure ready or something better than just a timeout
client client
.wait_until_deployment_ready( .wait_until_deployment_ready(
"grafana-grafana-deployment", "grafana-grafana-deployment".to_string(),
Some("grafana"), Some("grafana"),
Some(Duration::from_secs(30)), Some(30),
) )
.await?; .await?;

View File

@@ -9,7 +9,6 @@ use std::{
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use harmony_types::{ use harmony_types::{
id::Id,
net::{IpAddress, MacAddress}, net::{IpAddress, MacAddress},
switch::PortLocation, switch::PortLocation,
}; };
@@ -192,12 +191,15 @@ pub trait Switch: Send + Sync {
mac_address: &MacAddress, mac_address: &MacAddress,
) -> Result<Option<PortLocation>, SwitchError>; ) -> Result<Option<PortLocation>, SwitchError>;
async fn configure_host_network(&self, config: &HostNetworkConfig) -> Result<(), SwitchError>; async fn configure_host_network(
&self,
host: &PhysicalHost,
config: HostNetworkConfig,
) -> Result<(), SwitchError>;
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct HostNetworkConfig { pub struct HostNetworkConfig {
pub host_id: Id,
pub switch_ports: Vec<SwitchPort>, pub switch_ports: Vec<SwitchPort>,
} }

View File

@@ -1,7 +1,6 @@
use std::{any::Any, collections::HashMap}; use std::any::Any;
use async_trait::async_trait; use async_trait::async_trait;
use kube::api::DynamicObject;
use log::debug; use log::debug;
use crate::{ use crate::{
@@ -77,15 +76,6 @@ 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]

View File

@@ -14,7 +14,7 @@ use k8s_openapi::{
}, },
apimachinery::pkg::util::intstr::IntOrString, apimachinery::pkg::util::intstr::IntOrString,
}; };
use kube::{Resource, api::DynamicObject}; use kube::Resource;
use log::debug; use log::debug;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::json; use serde_json::json;

View File

@@ -11,7 +11,7 @@ pub struct InventoryRepositoryFactory;
impl InventoryRepositoryFactory { impl InventoryRepositoryFactory {
pub async fn build() -> Result<Box<dyn InventoryRepository>, RepoError> { pub async fn build() -> Result<Box<dyn InventoryRepository>, RepoError> {
Ok(Box::new( Ok(Box::new(
SqliteInventoryRepository::new(&DATABASE_URL).await?, SqliteInventoryRepository::new(&(*DATABASE_URL)).await?,
)) ))
} }
} }

View File

@@ -1,182 +0,0 @@
use k8s_openapi::Resource as K8sResource;
use kube::api::{ApiResource, DynamicObject, GroupVersionKind};
use kube::core::TypeMeta;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
/// Convert a typed Kubernetes resource `K` into a `DynamicObject`.
///
/// Requirements:
/// - `K` must be a k8s_openapi resource (provides static GVK via `Resource`).
/// - `K` must have standard Kubernetes shape (metadata + payload fields).
///
/// Notes:
/// - We set `types` (apiVersion/kind) and copy `metadata`.
/// - We place the remaining top-level fields into `obj.data` as JSON.
/// - Scope is not encoded on the object itself; you still need the corresponding
/// `DynamicResource` (derived from K::group/version/kind) when constructing an Api.
///
/// Example usage:
/// let dyn_obj = kube_resource_to_dynamic(secret)?;
/// let api: Api<DynamicObject> = Api::namespaced_with(client, "ns", &dr);
/// api.patch(&dyn_obj.name_any(), &PatchParams::apply("mgr"), &Patch::Apply(dyn_obj)).await?;
pub fn kube_resource_to_dynamic<K>(res: &K) -> Result<DynamicObject, String>
where
K: K8sResource + Serialize + DeserializeOwned,
{
// Serialize the typed resource to JSON so we can split metadata and payload
let mut v = serde_json::to_value(res).map_err(|e| format!("Failed to serialize : {e}"))?;
let obj = v
.as_object_mut()
.ok_or_else(|| "expected object JSON".to_string())?;
// Extract and parse metadata into kube::core::ObjectMeta
let metadata_value = obj
.remove("metadata")
.ok_or_else(|| "missing metadata".to_string())?;
let metadata: kube::core::ObjectMeta = serde_json::from_value(metadata_value)
.map_err(|e| format!("Failed to deserialize : {e}"))?;
// Name is required for DynamicObject::new; prefer metadata.name
let name = metadata
.name
.clone()
.ok_or_else(|| "metadata.name is required".to_string())?;
// Remaining fields (spec/status/data/etc.) become the dynamic payload
let payload = Value::Object(obj.clone());
// Construct the DynamicObject
let mut dyn_obj = DynamicObject::new(
&name,
&ApiResource::from_gvk(&GroupVersionKind::gvk(K::GROUP, K::VERSION, K::KIND)),
);
dyn_obj.types = Some(TypeMeta {
api_version: api_version_for::<K>(),
kind: K::KIND.into(),
});
// Preserve namespace/labels/annotations/etc.
dyn_obj.metadata = metadata;
// Attach payload
dyn_obj.data = payload;
Ok(dyn_obj)
}
/// Helper: compute apiVersion string ("group/version" or "v1" for core).
fn api_version_for<K>() -> String
where
K: K8sResource,
{
let group = K::GROUP;
let version = K::VERSION;
if group.is_empty() {
version.to_string() // core/v1 => "v1"
} else {
format!("{}/{}", group, version)
}
}
#[cfg(test)]
mod test {
use super::*;
use k8s_openapi::api::{
apps::v1::{Deployment, DeploymentSpec},
core::v1::{PodTemplateSpec, Secret},
};
use kube::api::ObjectMeta;
use pretty_assertions::assert_eq;
#[test]
fn secret_to_dynamic_roundtrip() {
// Create a sample Secret resource
let mut secret = Secret {
metadata: ObjectMeta {
name: Some("my-secret".to_string()),
..Default::default()
},
type_: Some("kubernetes.io/service-account-token".to_string()),
..Default::default()
};
// Convert to DynamicResource
let dynamic: DynamicObject =
kube_resource_to_dynamic(&secret).expect("Failed to convert Secret to DynamicResource");
// Serialize both the original and dynamic resources to Value
let original_value = serde_json::to_value(&secret).expect("Failed to serialize Secret");
let dynamic_value =
serde_json::to_value(&dynamic).expect("Failed to serialize DynamicResource");
// Assert that they are identical
assert_eq!(original_value, dynamic_value);
secret.metadata.namespace = Some("false".to_string());
let modified_value = serde_json::to_value(&secret).expect("Failed to serialize Secret");
assert_ne!(modified_value, dynamic_value);
}
#[test]
fn deployment_to_dynamic_roundtrip() {
// Create a sample Deployment with nested structures
let mut deployment = Deployment {
metadata: ObjectMeta {
name: Some("my-deployment".to_string()),
labels: Some({
let mut map = std::collections::BTreeMap::new();
map.insert("app".to_string(), "nginx".to_string());
map
}),
..Default::default()
},
spec: Some(DeploymentSpec {
replicas: Some(3),
selector: Default::default(),
template: PodTemplateSpec {
metadata: Some(ObjectMeta {
labels: Some({
let mut map = std::collections::BTreeMap::new();
map.insert("app".to_string(), "nginx".to_string());
map
}),
..Default::default()
}),
spec: Some(Default::default()), // PodSpec with empty containers for simplicity
},
..Default::default()
}),
..Default::default()
};
let dynamic = kube_resource_to_dynamic(&deployment).expect("Failed to convert Deployment");
let original_value = serde_json::to_value(&deployment).unwrap();
let dynamic_value = serde_json::to_value(&dynamic).unwrap();
assert_eq!(original_value, dynamic_value);
assert_eq!(
dynamic.data.get("spec").unwrap().get("replicas").unwrap(),
3
);
assert_eq!(
dynamic
.data
.get("spec")
.unwrap()
.get("template")
.unwrap()
.get("metadata")
.unwrap()
.get("labels")
.unwrap()
.get("app")
.unwrap()
.as_str()
.unwrap(),
"nginx".to_string()
);
}
}

View File

@@ -3,6 +3,5 @@ pub mod executors;
pub mod hp_ilo; pub mod hp_ilo;
pub mod intel_amt; pub mod intel_amt;
pub mod inventory; pub mod inventory;
pub mod kube;
pub mod opnsense; pub mod opnsense;
mod sqlx; mod sqlx;

View File

@@ -38,15 +38,13 @@ impl<
+ 'static + 'static
+ Send + Send
+ Clone, + Clone,
T: Topology + K8sclient, T: Topology,
> Score<T> for K8sResourceScore<K> > Score<T> for K8sResourceScore<K>
where where
<K as kube::Resource>::DynamicType: Default, <K as kube::Resource>::DynamicType: Default,
{ {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(K8sResourceInterpret { todo!()
score: self.clone(),
})
} }
fn name(&self) -> String { fn name(&self) -> String {

View File

@@ -1,23 +1,18 @@
use std::any::Any; use std::any::Any;
use std::collections::{BTreeMap, HashMap}; use std::collections::BTreeMap;
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::Resource; use kube::api::ObjectMeta;
use kube::api::{DynamicObject, ObjectMeta}; use log::debug;
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::{
@@ -33,13 +28,14 @@ use harmony_types::net::Url;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct DiscordWebhook { pub struct DiscordWebhook {
pub name: K8sName, pub name: String,
pub url: Url, pub url: Url,
pub selectors: Vec<HashMap<String, String>>,
} }
impl DiscordWebhook { #[async_trait]
fn get_receiver_config(&self) -> Result<AlertManagerReceiver, String> { impl AlertReceiver<RHOBObservability> for DiscordWebhook {
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());
@@ -56,91 +52,33 @@ impl DiscordWebhook {
..Default::default() ..Default::default()
}; };
let mut matchers: Vec<String> = Vec::new(); let _ = sender.client.apply(&secret, Some(&ns)).await;
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().to_string()), name: Some(self.name.clone()),
labels: Some(std::collections::BTreeMap::from([( labels: Some(std::collections::BTreeMap::from([(
"alertmanagerConfig".to_string(), "alertmanagerConfig".to_string(),
"enabled".to_string(), "enabled".to_string(),
@@ -184,9 +122,6 @@ 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());
@@ -232,7 +167,7 @@ impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
let alertmanager_configs = AlertmanagerConfig { let alertmanager_configs = AlertmanagerConfig {
metadata: ObjectMeta { metadata: ObjectMeta {
name: Some(self.name.clone().to_string()), name: Some(self.name.clone()),
labels: Some(std::collections::BTreeMap::from([( labels: Some(std::collections::BTreeMap::from([(
"alertmanagerConfig".to_string(), "alertmanagerConfig".to_string(),
"enabled".to_string(), "enabled".to_string(),
@@ -265,9 +200,6 @@ 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
} }
@@ -285,7 +217,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().to_string() self.name.clone()
} }
async fn configure_receiver(&self) -> AlertManagerChannelConfig { async fn configure_receiver(&self) -> AlertManagerChannelConfig {
self.get_config().await self.get_config().await
@@ -294,9 +226,6 @@ 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
} }
@@ -314,7 +243,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().to_string() self.name.clone()
} }
async fn configure_receiver(&self) -> AlertManagerChannelConfig { async fn configure_receiver(&self) -> AlertManagerChannelConfig {
self.get_config().await self.get_config().await
@@ -341,7 +270,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().to_string()), Value::String(self.name.clone()),
); );
route.insert( route.insert(
Value::String("matchers".to_string()), Value::String("matchers".to_string()),
@@ -355,7 +284,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().to_string()), Value::String(self.name.clone()),
); );
let mut discord_config = Mapping::new(); let mut discord_config = Mapping::new();
@@ -380,9 +309,8 @@ 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: K8sName("test-discord".to_string()), name: "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 =

View File

@@ -19,7 +19,7 @@ use crate::{
}, },
prometheus::prometheus::{Prometheus, PrometheusReceiver}, prometheus::prometheus::{Prometheus, PrometheusReceiver},
}, },
topology::oberservability::monitoring::{AlertManagerReceiver, AlertReceiver}, topology::oberservability::monitoring::AlertReceiver,
}; };
use harmony_types::net::Url; use harmony_types::net::Url;
@@ -31,9 +31,6 @@ 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!({
@@ -100,9 +97,6 @@ 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!({
@@ -164,9 +158,6 @@ 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
} }
@@ -193,9 +184,6 @@ 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
} }

View File

@@ -14,8 +14,7 @@ use crate::{
}, },
score::Score, score::Score,
topology::{ topology::{
K8sclient, Topology, oberservability::monitoring::{AlertReceiver, AlertingInterpret, ScrapeTarget}, K8sclient, Topology
oberservability::monitoring::{AlertReceiver, AlertingInterpret, ScrapeTarget},
}, },
}; };

View File

@@ -15,10 +15,7 @@ use crate::{
prometheus::prometheus::PrometheusMonitoring, prometheus::prometheus::PrometheusMonitoring,
}, },
topology::{ topology::{
K8sclient, Topology, installable::Installable, k8s::K8sClient, oberservability::monitoring::{AlertReceiver, AlertSender, ScrapeTarget}, K8sclient, Topology
installable::Installable,
k8s::K8sClient,
oberservability::monitoring::{AlertReceiver, AlertSender, ScrapeTarget},
}, },
}; };

View File

@@ -100,7 +100,11 @@ impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Interpret<T> f
info!("deploying ntfy..."); info!("deploying ntfy...");
client client
.wait_until_deployment_ready("ntfy", Some(self.score.namespace.as_str()), None) .wait_until_deployment_ready(
"ntfy".to_string(),
Some(self.score.namespace.as_str()),
None,
)
.await?; .await?;
info!("ntfy deployed"); info!("ntfy deployed");

View File

@@ -1,270 +0,0 @@
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!()
}
}

View File

@@ -1,90 +0,0 @@
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
)))
}
}

View File

@@ -1,13 +1,16 @@
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}, topology::{K8sclient, Topology, k8s::K8sClient},
}; };
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)]
@@ -34,9 +37,10 @@ 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();
Config::create_cluster_monitoring_config_cm(&client).await?; self.update_cluster_monitoring_config_cm(&client).await?;
Config::create_user_workload_monitoring_config_cm(&client).await?; self.update_user_workload_monitoring_config_cm(&client)
Config::verify_user_workload(&client).await?; .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(),
)) ))
@@ -58,3 +62,88 @@ 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
)))
}
}

View File

@@ -1,14 +1 @@
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()
}
}

View File

@@ -5,8 +5,10 @@ use crate::{
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::{ modules::{
dhcp::DhcpHostBindingScore, http::IPxeMacBootFileScore, dhcp::DhcpHostBindingScore,
inventory::DiscoverHostForRoleScore, okd::templates::BootstrapIpxeTpl, http::IPxeMacBootFileScore,
inventory::DiscoverHostForRoleScore,
okd::{host_network::HostNetworkConfigurationScore, templates::BootstrapIpxeTpl},
}, },
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::{HAClusterTopology, HostBinding},
@@ -203,6 +205,28 @@ impl OKDSetup03ControlPlaneInterpret {
Ok(()) Ok(())
} }
/// Placeholder for automating network bonding configuration.
async fn persist_network_bond(
&self,
inventory: &Inventory,
topology: &HAClusterTopology,
hosts: &Vec<PhysicalHost>,
) -> Result<(), InterpretError> {
info!("[ControlPlane] Ensuring persistent bonding");
let score = HostNetworkConfigurationScore {
hosts: hosts.clone(),
};
score.interpret(inventory, topology).await?;
inquire::Confirm::new(
"Network configuration for control plane nodes is not automated yet. Configure it manually if needed.",
)
.prompt()
.map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
Ok(())
}
} }
#[async_trait] #[async_trait]
@@ -241,6 +265,10 @@ impl Interpret<HAClusterTopology> for OKDSetup03ControlPlaneInterpret {
// 4. Reboot the nodes to start the OS installation. // 4. Reboot the nodes to start the OS installation.
self.reboot_targets(&nodes).await?; self.reboot_targets(&nodes).await?;
// 5. Placeholder for post-boot network configuration (e.g., bonding).
self.persist_network_bond(inventory, topology, &nodes)
.await?;
// TODO: Implement a step to wait for the control plane nodes to join the cluster // TODO: Implement a step to wait for the control plane nodes to join the cluster
// and for the cluster operators to become available. This would be similar to // and for the cluster operators to become available. This would be similar to
// the `wait-for bootstrap-complete` command. // the `wait-for bootstrap-complete` command.

View File

@@ -1,130 +0,0 @@
use crate::{
data::Version,
hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory},
modules::okd::host_network::HostNetworkConfigurationScore,
score::Score,
topology::HAClusterTopology,
};
use async_trait::async_trait;
use derive_new::new;
use harmony_types::id::Id;
use log::info;
use serde::Serialize;
// -------------------------------------------------------------------------------------------------
// Persist Network Bond
// - Persist bonding via NMState
// - Persist port channels on the Switch
// -------------------------------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, new)]
pub struct OKDSetupPersistNetworkBondScore {}
impl Score<HAClusterTopology> for OKDSetupPersistNetworkBondScore {
fn create_interpret(&self) -> Box<dyn Interpret<HAClusterTopology>> {
Box::new(OKDSetupPersistNetworkBondInterpet::new())
}
fn name(&self) -> String {
"OKDSetupPersistNetworkBondScore".to_string()
}
}
#[derive(Debug, Clone)]
pub struct OKDSetupPersistNetworkBondInterpet {
version: Version,
status: InterpretStatus,
}
impl OKDSetupPersistNetworkBondInterpet {
pub fn new() -> Self {
let version = Version::from("1.0.0").unwrap();
Self {
version,
status: InterpretStatus::QUEUED,
}
}
/// Ensures that three physical hosts are discovered and available for the ControlPlane role.
/// It will trigger discovery if not enough hosts are found.
async fn get_nodes(
&self,
_inventory: &Inventory,
_topology: &HAClusterTopology,
) -> Result<Vec<PhysicalHost>, InterpretError> {
const REQUIRED_HOSTS: usize = 3;
let repo = InventoryRepositoryFactory::build().await?;
let control_plane_hosts = repo.get_host_for_role(&HostRole::ControlPlane).await?;
if control_plane_hosts.len() < REQUIRED_HOSTS {
Err(InterpretError::new(format!(
"OKD Requires at least {} control plane hosts, but only found {}. Cannot proceed.",
REQUIRED_HOSTS,
control_plane_hosts.len()
)))
} else {
// Take exactly the number of required hosts to ensure consistency.
Ok(control_plane_hosts
.into_iter()
.take(REQUIRED_HOSTS)
.collect())
}
}
async fn persist_network_bond(
&self,
inventory: &Inventory,
topology: &HAClusterTopology,
hosts: &Vec<PhysicalHost>,
) -> Result<(), InterpretError> {
info!("Ensuring persistent bonding");
let score = HostNetworkConfigurationScore {
hosts: hosts.clone(),
};
score.interpret(inventory, topology).await?;
Ok(())
}
}
#[async_trait]
impl Interpret<HAClusterTopology> for OKDSetupPersistNetworkBondInterpet {
fn get_name(&self) -> InterpretName {
InterpretName::Custom("OKDSetupPersistNetworkBondInterpet")
}
fn get_version(&self) -> Version {
self.version.clone()
}
fn get_status(&self) -> InterpretStatus {
self.status.clone()
}
fn get_children(&self) -> Vec<Id> {
vec![]
}
async fn execute(
&self,
inventory: &Inventory,
topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> {
let nodes = self.get_nodes(inventory, topology).await?;
let res = self.persist_network_bond(inventory, topology, &nodes).await;
match res {
Ok(_) => Ok(Outcome::success(
"Network bond successfully persisted".into(),
)),
Err(_) => Err(InterpretError::new(
"Failed to persist network bond".to_string(),
)),
}
}
}

View File

@@ -1 +1,41 @@
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub mod nmstate; pub mod nmstate;
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(
group = "operators.coreos.com",
version = "v1",
kind = "OperatorGroup",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct OperatorGroupSpec {
pub target_namespaces: Vec<String>,
}
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(
group = "operators.coreos.com",
version = "v1alpha1",
kind = "Subscription",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionSpec {
pub name: String,
pub source: String,
pub source_namespace: String,
pub channel: Option<String>,
pub install_plan_approval: Option<InstallPlanApproval>,
}
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
pub enum InstallPlanApproval {
#[serde(rename = "Automatic")]
Automatic,
#[serde(rename = "Manual")]
Manual,
}

View File

@@ -6,16 +6,9 @@ use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube( #[kube(group = "nmstate.io", version = "v1", kind = "NMState", namespaced)]
group = "nmstate.io",
version = "v1",
kind = "NMState",
plural = "nmstates",
namespaced = false
)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NMStateSpec { pub struct NMStateSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub probe_configuration: Option<ProbeConfig>, pub probe_configuration: Option<ProbeConfig>,
} }
@@ -51,7 +44,6 @@ pub struct ProbeDns {
)] )]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NodeNetworkConfigurationPolicySpec { pub struct NodeNetworkConfigurationPolicySpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub node_selector: Option<BTreeMap<String, String>>, pub node_selector: Option<BTreeMap<String, String>>,
pub desired_state: DesiredStateSpec, pub desired_state: DesiredStateSpec,
} }
@@ -66,64 +58,37 @@ pub struct DesiredStateSpec {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct InterfaceSpec { pub struct InterfaceSpec {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
pub r#type: String, pub r#type: String,
pub state: String, pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_address: Option<String>, pub mac_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub copy_mac_from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtu: Option<u32>, pub mtu: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<String>, pub controller: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv4: Option<IpStackSpec>, pub ipv4: Option<IpStackSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv6: Option<IpStackSpec>, pub ipv6: Option<IpStackSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ethernet: Option<EthernetSpec>, pub ethernet: Option<EthernetSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_aggregation: Option<BondSpec>, pub link_aggregation: Option<BondSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vlan: Option<VlanSpec>, pub vlan: Option<VlanSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vxlan: Option<VxlanSpec>, pub vxlan: Option<VxlanSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_vtap: Option<MacVtapSpec>, pub mac_vtap: Option<MacVtapSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_vlan: Option<MacVlanSpec>, pub mac_vlan: Option<MacVlanSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub infiniband: Option<InfinibandSpec>, pub infiniband: Option<InfinibandSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub linux_bridge: Option<LinuxBridgeSpec>, pub linux_bridge: Option<LinuxBridgeSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ovs_bridge: Option<OvsBridgeSpec>, pub ovs_bridge: Option<OvsBridgeSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ethtool: Option<EthtoolSpec>, pub ethtool: Option<EthtoolSpec>,
} }
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct IpStackSpec { pub struct IpStackSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>, pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dhcp: Option<bool>, pub dhcp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autoconf: Option<bool>, pub autoconf: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<Vec<IpAddressSpec>>, pub address: Option<Vec<IpAddressSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_dns: Option<bool>, pub auto_dns: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_gateway: Option<bool>, pub auto_gateway: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_routes: Option<bool>, pub auto_routes: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dhcp_client_id: Option<String>, pub dhcp_client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dhcp_duid: Option<String>, pub dhcp_duid: Option<String>,
} }
@@ -137,11 +102,8 @@ pub struct IpAddressSpec {
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct EthernetSpec { pub struct EthernetSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<u32>, pub speed: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duplex: Option<String>, pub duplex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_negotiation: Option<bool>, pub auto_negotiation: Option<bool>,
} }
@@ -150,7 +112,6 @@ pub struct EthernetSpec {
pub struct BondSpec { pub struct BondSpec {
pub mode: String, pub mode: String,
pub ports: Vec<String>, pub ports: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<BTreeMap<String, Value>>, pub options: Option<BTreeMap<String, Value>>,
} }
@@ -159,7 +120,6 @@ pub struct BondSpec {
pub struct VlanSpec { pub struct VlanSpec {
pub base_iface: String, pub base_iface: String,
pub id: u16, pub id: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<String>, pub protocol: Option<String>,
} }
@@ -169,11 +129,8 @@ pub struct VxlanSpec {
pub base_iface: String, pub base_iface: String,
pub id: u32, pub id: u32,
pub remote: String, pub remote: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<String>, pub local: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub learning: Option<bool>, pub learning: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination_port: Option<u16>, pub destination_port: Option<u16>,
} }
@@ -182,7 +139,6 @@ pub struct VxlanSpec {
pub struct MacVtapSpec { pub struct MacVtapSpec {
pub base_iface: String, pub base_iface: String,
pub mode: String, pub mode: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub promiscuous: Option<bool>, pub promiscuous: Option<bool>,
} }
@@ -191,7 +147,6 @@ pub struct MacVtapSpec {
pub struct MacVlanSpec { pub struct MacVlanSpec {
pub base_iface: String, pub base_iface: String,
pub mode: String, pub mode: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub promiscuous: Option<bool>, pub promiscuous: Option<bool>,
} }
@@ -206,35 +161,25 @@ pub struct InfinibandSpec {
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LinuxBridgeSpec { pub struct LinuxBridgeSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<LinuxBridgeOptions>, pub options: Option<LinuxBridgeOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ports: Option<Vec<LinuxBridgePort>>, pub ports: Option<Vec<LinuxBridgePort>>,
} }
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LinuxBridgeOptions { pub struct LinuxBridgeOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_ageing_time: Option<u32>, pub mac_ageing_time: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multicast_snooping: Option<bool>, pub multicast_snooping: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stp: Option<StpOptions>, pub stp: Option<StpOptions>,
} }
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct StpOptions { pub struct StpOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>, pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub forward_delay: Option<u16>, pub forward_delay: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hello_time: Option<u16>, pub hello_time: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age: Option<u16>, pub max_age: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<u16>, pub priority: Option<u16>,
} }
@@ -242,20 +187,15 @@ pub struct StpOptions {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LinuxBridgePort { pub struct LinuxBridgePort {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub vlan: Option<LinuxBridgePortVlan>, pub vlan: Option<LinuxBridgePortVlan>,
} }
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LinuxBridgePortVlan { pub struct LinuxBridgePortVlan {
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<String>, pub mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trunk_tags: Option<Vec<VlanTag>>, pub trunk_tags: Option<Vec<VlanTag>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<u16>, pub tag: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_native: Option<bool>, pub enable_native: Option<bool>,
} }
@@ -263,7 +203,6 @@ pub struct LinuxBridgePortVlan {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct VlanTag { pub struct VlanTag {
pub id: u16, pub id: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_range: Option<VlanIdRange>, pub id_range: Option<VlanIdRange>,
} }
@@ -277,20 +216,15 @@ pub struct VlanIdRange {
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct OvsBridgeSpec { pub struct OvsBridgeSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<OvsBridgeOptions>, pub options: Option<OvsBridgeOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ports: Option<Vec<OvsPortSpec>>, pub ports: Option<Vec<OvsPortSpec>>,
} }
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct OvsBridgeOptions { pub struct OvsBridgeOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub stp: Option<bool>, pub stp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rstp: Option<bool>, pub rstp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcast_snooping_enable: Option<bool>, pub mcast_snooping_enable: Option<bool>,
} }
@@ -298,11 +232,8 @@ pub struct OvsBridgeOptions {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct OvsPortSpec { pub struct OvsPortSpec {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_aggregation: Option<BondSpec>, pub link_aggregation: Option<BondSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vlan: Option<LinuxBridgePortVlan>, pub vlan: Option<LinuxBridgePortVlan>,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<String>, pub r#type: Option<String>,
} }
@@ -315,8 +246,6 @@ pub struct EthtoolSpec {
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] #[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct EthtoolFecSpec { pub struct EthtoolFecSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub auto: Option<bool>, pub auto: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<String>, pub mode: Option<String>,
} }

View File

@@ -39,70 +39,30 @@ impl HostNetworkConfigurationInterpret {
&self, &self,
topology: &T, topology: &T,
host: &PhysicalHost, host: &PhysicalHost,
current_host: &usize, ) -> Result<(), InterpretError> {
total_hosts: &usize, let switch_ports = self.collect_switch_ports_for_host(topology, host).await?;
) -> Result<HostNetworkConfig, InterpretError> { if !switch_ports.is_empty() {
if host.network.is_empty() {
info!("[Host {current_host}/{total_hosts}] No interfaces to configure, skipping");
return Ok(HostNetworkConfig {
host_id: host.id.clone(),
switch_ports: vec![],
});
}
let switch_ports = self
.collect_switch_ports_for_host(topology, host, current_host, total_hosts)
.await?;
let config = HostNetworkConfig {
host_id: host.id.clone(),
switch_ports,
};
if !config.switch_ports.is_empty() {
info!(
"[Host {current_host}/{total_hosts}] Found {} ports for {} interfaces",
config.switch_ports.len(),
host.network.len()
);
info!("[Host {current_host}/{total_hosts}] Configuring host network...");
topology topology
.configure_host_network(&config) .configure_host_network(host, HostNetworkConfig { switch_ports })
.await .await
.map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?; .map_err(|e| InterpretError::new(format!("Failed to configure host: {e}")))?;
} else {
info!(
"[Host {current_host}/{total_hosts}] No ports found for {} interfaces, skipping",
host.network.len()
);
} }
Ok(config) Ok(())
} }
async fn collect_switch_ports_for_host<T: Topology + Switch>( async fn collect_switch_ports_for_host<T: Topology + Switch>(
&self, &self,
topology: &T, topology: &T,
host: &PhysicalHost, host: &PhysicalHost,
current_host: &usize,
total_hosts: &usize,
) -> Result<Vec<SwitchPort>, InterpretError> { ) -> Result<Vec<SwitchPort>, InterpretError> {
let mut switch_ports = vec![]; let mut switch_ports = vec![];
if host.network.is_empty() {
return Ok(switch_ports);
}
info!("[Host {current_host}/{total_hosts}] Collecting ports on switch...");
for network_interface in &host.network { for network_interface in &host.network {
let mac_address = network_interface.mac_address; let mac_address = network_interface.mac_address;
match topology.get_port_for_mac_address(&mac_address).await { match topology.get_port_for_mac_address(&mac_address).await {
Ok(Some(port)) => { Ok(Some(port)) => {
info!(
"[Host {current_host}/{total_hosts}] Found port '{port}' for '{mac_address}'"
);
switch_ports.push(SwitchPort { switch_ports.push(SwitchPort {
interface: NetworkInterface { interface: NetworkInterface {
name: network_interface.name.clone(), name: network_interface.name.clone(),
@@ -113,7 +73,7 @@ impl HostNetworkConfigurationInterpret {
port, port,
}); });
} }
Ok(None) => debug!("No port found for '{mac_address}', skipping"), Ok(None) => debug!("No port found for host '{}', skipping", host.id),
Err(e) => { Err(e) => {
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"Failed to get port for host '{}': {}", "Failed to get port for host '{}': {}",
@@ -125,47 +85,6 @@ impl HostNetworkConfigurationInterpret {
Ok(switch_ports) Ok(switch_ports)
} }
fn format_host_configuration(&self, configs: Vec<HostNetworkConfig>) -> Vec<String> {
let mut report = vec![
"Network Configuration Report".to_string(),
"------------------------------------------------------------------".to_string(),
];
for config in configs {
let host = self
.score
.hosts
.iter()
.find(|h| h.id == config.host_id)
.unwrap();
println!("[Host] {host}");
if config.switch_ports.is_empty() {
report.push(format!(
"⏭️ Host {}: SKIPPED (No matching switch ports found)",
config.host_id
));
} else {
let mappings: Vec<String> = config
.switch_ports
.iter()
.map(|p| format!("[{} -> {}]", p.interface.name, p.port))
.collect();
report.push(format!(
"✅ Host {}: Bonded {} port(s) {}",
config.host_id,
config.switch_ports.len(),
mappings.join(", ")
));
}
}
report
.push("------------------------------------------------------------------".to_string());
report
}
} }
#[async_trait] #[async_trait]
@@ -195,38 +114,27 @@ impl<T: Topology + Switch> Interpret<T> for HostNetworkConfigurationInterpret {
return Ok(Outcome::noop("No hosts to configure".into())); return Ok(Outcome::noop("No hosts to configure".into()));
} }
let host_count = self.score.hosts.len(); info!(
info!("Started network configuration for {host_count} host(s)...",); "Started network configuration for {} host(s)...",
self.score.hosts.len()
);
info!("Setting up switch with sane defaults...");
topology topology
.setup_switch() .setup_switch()
.await .await
.map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?; .map_err(|e| InterpretError::new(format!("Switch setup failed: {e}")))?;
info!("Switch ready");
let mut current_host = 1;
let mut host_configurations = vec![];
let mut configured_host_count = 0;
for host in &self.score.hosts { for host in &self.score.hosts {
let host_configuration = self self.configure_network_for_host(topology, host).await?;
.configure_network_for_host(topology, host, &current_host, &host_count) configured_host_count += 1;
.await?;
host_configurations.push(host_configuration);
current_host += 1;
} }
if current_host > 1 {
let details = self.format_host_configuration(host_configurations);
Ok(Outcome::success_with_details( if configured_host_count > 0 {
format!( Ok(Outcome::success(format!(
"Configured {}/{} host(s)", "Configured {configured_host_count}/{} host(s)",
current_host - 1,
self.score.hosts.len() self.score.hosts.len()
), )))
details,
))
} else { } else {
Ok(Outcome::noop("No hosts configured".into())) Ok(Outcome::noop("No hosts configured".into()))
} }
@@ -301,7 +209,6 @@ mod tests {
assert_that!(*configured_host_networks).contains_exactly(vec![( assert_that!(*configured_host_networks).contains_exactly(vec![(
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![SwitchPort {
interface: EXISTING_INTERFACE.clone(), interface: EXISTING_INTERFACE.clone(),
port: PORT.clone(), port: PORT.clone(),
@@ -327,7 +234,6 @@ mod tests {
assert_that!(*configured_host_networks).contains_exactly(vec![( assert_that!(*configured_host_networks).contains_exactly(vec![(
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: HOST_ID.clone(),
switch_ports: vec![ switch_ports: vec![
SwitchPort { SwitchPort {
interface: EXISTING_INTERFACE.clone(), interface: EXISTING_INTERFACE.clone(),
@@ -357,7 +263,6 @@ mod tests {
( (
HOST_ID.clone(), HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![SwitchPort {
interface: EXISTING_INTERFACE.clone(), interface: EXISTING_INTERFACE.clone(),
port: PORT.clone(), port: PORT.clone(),
@@ -367,7 +272,6 @@ mod tests {
( (
ANOTHER_HOST_ID.clone(), ANOTHER_HOST_ID.clone(),
HostNetworkConfig { HostNetworkConfig {
host_id: ANOTHER_HOST_ID.clone(),
switch_ports: vec![SwitchPort { switch_ports: vec![SwitchPort {
interface: ANOTHER_EXISTING_INTERFACE.clone(), interface: ANOTHER_EXISTING_INTERFACE.clone(),
port: ANOTHER_PORT.clone(), port: ANOTHER_PORT.clone(),
@@ -478,10 +382,11 @@ mod tests {
async fn configure_host_network( async fn configure_host_network(
&self, &self,
config: &HostNetworkConfig, host: &PhysicalHost,
config: HostNetworkConfig,
) -> Result<(), SwitchError> { ) -> Result<(), SwitchError> {
let mut configured_host_networks = self.configured_host_networks.lock().unwrap(); let mut configured_host_networks = self.configured_host_networks.lock().unwrap();
configured_host_networks.push((config.host_id.clone(), config.clone())); configured_host_networks.push((host.id.clone(), config.clone()));
Ok(()) Ok(())
} }

View File

@@ -50,7 +50,7 @@
use crate::{ use crate::{
modules::okd::{ modules::okd::{
OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore, OKDSetup01InventoryScore, OKDSetup02BootstrapScore, OKDSetup03ControlPlaneScore,
OKDSetup04WorkersScore, OKDSetup05SanityCheckScore, OKDSetupPersistNetworkBondScore, OKDSetup04WorkersScore, OKDSetup05SanityCheckScore,
bootstrap_06_installation_report::OKDSetup06InstallationReportScore, bootstrap_06_installation_report::OKDSetup06InstallationReportScore,
}, },
score::Score, score::Score,
@@ -65,7 +65,6 @@ impl OKDInstallationPipeline {
Box::new(OKDSetup01InventoryScore::new()), Box::new(OKDSetup01InventoryScore::new()),
Box::new(OKDSetup02BootstrapScore::new()), Box::new(OKDSetup02BootstrapScore::new()),
Box::new(OKDSetup03ControlPlaneScore::new()), Box::new(OKDSetup03ControlPlaneScore::new()),
Box::new(OKDSetupPersistNetworkBondScore::new()),
Box::new(OKDSetup04WorkersScore::new()), Box::new(OKDSetup04WorkersScore::new()),
Box::new(OKDSetup05SanityCheckScore::new()), Box::new(OKDSetup05SanityCheckScore::new()),
Box::new(OKDSetup06InstallationReportScore::new()), Box::new(OKDSetup06InstallationReportScore::new()),

View File

@@ -6,7 +6,6 @@ mod bootstrap_05_sanity_check;
mod bootstrap_06_installation_report; mod bootstrap_06_installation_report;
pub mod bootstrap_dhcp; pub mod bootstrap_dhcp;
pub mod bootstrap_load_balancer; pub mod bootstrap_load_balancer;
mod bootstrap_persist_network_bond;
pub mod dhcp; pub mod dhcp;
pub mod dns; pub mod dns;
pub mod installation; pub mod installation;
@@ -20,6 +19,5 @@ pub use bootstrap_03_control_plane::*;
pub use bootstrap_04_workers::*; pub use bootstrap_04_workers::*;
pub use bootstrap_05_sanity_check::*; pub use bootstrap_05_sanity_check::*;
pub use bootstrap_06_installation_report::*; pub use bootstrap_06_installation_report::*;
pub use bootstrap_persist_network_bond::*;
pub mod crd; pub mod crd;
pub mod host_network; pub mod host_network;

View File

@@ -40,7 +40,7 @@ pub fn init() {
HarmonyEvent::HarmonyFinished => { HarmonyEvent::HarmonyFinished => {
if !details.is_empty() { if !details.is_empty() {
println!( println!(
"\n{} All done! Here's a few info for you:", "\n{} All done! Here's what's next for you:",
theme::EMOJI_SUMMARY theme::EMOJI_SUMMARY
); );
for detail in details.iter() { for detail in details.iter() {

View File

@@ -1,96 +0,0 @@
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());
}
}

View File

@@ -1,4 +1,3 @@
pub mod id; pub mod id;
pub mod k8s_name;
pub mod net; pub mod net;
pub mod switch; pub mod switch;

View File

@@ -9,7 +9,7 @@ pub struct Interface {
pub physical_interface_name: String, pub physical_interface_name: String,
pub descr: Option<MaybeString>, pub descr: Option<MaybeString>,
pub mtu: Option<MaybeString>, pub mtu: Option<MaybeString>,
pub enable: Option<MaybeString>, pub enable: MaybeString,
pub lock: Option<MaybeString>, pub lock: Option<MaybeString>,
#[yaserde(rename = "spoofmac")] #[yaserde(rename = "spoofmac")]
pub spoof_mac: Option<MaybeString>, pub spoof_mac: Option<MaybeString>,
@@ -134,15 +134,19 @@ mod test {
<interfaces> <interfaces>
<paul> <paul>
<if></if> <if></if>
<enable/>
</paul> </paul>
<anotherpaul> <anotherpaul>
<if></if> <if></if>
<enable/>
</anotherpaul> </anotherpaul>
<thirdone> <thirdone>
<if></if> <if></if>
<enable/>
</thirdone> </thirdone>
<andgofor4> <andgofor4>
<if></if> <if></if>
<enable/>
</andgofor4> </andgofor4>
</interfaces> </interfaces>
<bar>foo</bar> <bar>foo</bar>