use std::net::Ipv4Addr; use std::path::PathBuf; use std::str::FromStr; use async_walkdir::WalkDir; use futures::StreamExt; use futures::future::join_all; use futures::stream::FuturesUnordered; use minijinja::Environment; use optional_struct::{Applicable, optional_struct}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tokio::process::Command; use crate::environment::PathEnvironment; use crate::node::{Node, OptionalNodeDeserialize}; use crate::patch::Patches; use crate::{get_configs_path, get_talos_path}; #[optional_struct] #[derive(Debug, Deserialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub(crate) struct Base { #[serde(default)] pub(crate) kernel_args: Vec, #[serde(default)] pub(crate) patches: Patches, } #[optional_struct] #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Version { kubernetes: semver::Version, talos: semver::Version, } impl Version { pub(crate) fn kubernetes(&self) -> String { format!("v{}", self.kubernetes) } pub(crate) fn talos(&self) -> String { format!("v{}", self.talos) } } #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub enum ClusterEnv { Production, Staging, } #[optional_struct] #[derive(Debug, Deserialize, JsonSchema, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct ClusterDeserialize { #[optional_rename(OptionalVersion)] #[optional_wrap] version: Version, nodes: Vec, cluster_env: ClusterEnv, control_plane_ip: Ipv4Addr, secrets_file: PathBuf, #[serde(default)] #[optional_skip_wrap] default: OptionalNodeDeserialize, #[optional_rename(OptionalBase)] #[optional_wrap] base: Base, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Cluster { pub(crate) name: String, pub(crate) version: Version, nodes: Vec, cluster_env: ClusterEnv, pub(crate) control_plane_ip: Ipv4Addr, pub(crate) secrets_file: PathBuf, } impl Cluster { pub(crate) async fn get(cluster_name: &str, env: &Environment<'static>) -> Self { let path = get_talos_path().join("clusters").join("default.yaml"); let default: OptionalClusterDeserialize = if path.exists() { let content = tokio::fs::read_to_string(path).await.unwrap(); // The deserialization makes a blocking web request tokio::task::spawn_blocking(move || serde_yaml::from_str(&content).unwrap()) .await .unwrap() } else { Default::default() }; let mut path = get_talos_path().join("clusters").join(cluster_name); path.add_extension("yaml"); let content = tokio::fs::read_to_string(path).await.unwrap(); let mut cluster: OptionalClusterDeserialize = serde_yaml::from_str(&content).unwrap(); // For some reason apply on the cluster does not properly apply to the default settings... // So we manually apply it here first cluster.default = default.default.clone().apply(cluster.default); let cluster: ClusterDeserialize = default.apply(cluster).try_into().unwrap(); let nodes = cluster.nodes; let default = cluster.default; let base = cluster.base; let secrets_file = get_talos_path() .join("secrets") .join(cluster.secrets_file) .absolute() .unwrap(); let cluster = Self { name: cluster_name.into(), version: cluster.version, nodes: vec![], cluster_env: cluster.cluster_env, control_plane_ip: cluster.control_plane_ip, secrets_file, }; let nodes = nodes .iter() .map(|name| async { Node::get(name, env, &cluster, &default, &base).await }); let nodes = FuturesUnordered::from_iter(nodes).collect().await; Self { nodes, ..cluster } } pub async fn get_all() -> Vec { let env = PathEnvironment::new_patches(); let names: Vec<_> = WalkDir::new(get_talos_path().join("clusters")) .filter_map(|e| async { e.ok() }) .filter(|e| { let e = e.clone(); async move { e.metadata().await.unwrap().is_file() } }) .filter_map(|e| async move { let mut path = PathBuf::from_str(&e.file_name().to_string_lossy()).unwrap(); path.set_extension(""); let name = path.to_string_lossy().to_string(); if name == "default" { return None; } Some(name) }) .collect() .await; // Get the clusters concurrently join_all(names.iter().map(|name| { let env = env.clone(); let name = name.clone(); async move { Cluster::get(&name, &env).await } })) .await } pub fn nodes(&self) -> &[Node] { &self.nodes } pub fn talosctl_gen_config_command(&self) -> Command { let path = get_configs_path().join(&self.name).join("talosconfig"); let mut command = Command::new("talosctl"); command.args([ "gen", "config", &self.name, &format!("https://{}:6443", self.control_plane_ip), "--with-secrets", self.secrets_file.to_str().expect("Path should be valid"), "--output-types", "talosconfig", "-o", path.to_str().expect("Path should be valid utf-8"), ]); command } pub fn talosctl_add_endpoint_command(&self) -> Command { let path = get_configs_path().join(&self.name).join("talosconfig"); let mut command = Command::new("talosctl"); command.args([ "config", "--talosconfig", path.to_str().expect("Path should be valid utf-8"), "endpoint", &self.control_plane_ip.to_string(), ]); command } pub fn talosctl_merge_command(&self) -> Command { let cluster_path = get_configs_path().join(&self.name).join("talosconfig"); let path = get_configs_path().join("talosconfig"); let mut command = Command::new("talosctl"); command.args([ "config", "--talosconfig", path.to_str().expect("Path should be valid utf-8"), "merge", cluster_path.to_str().expect("Path should be valid utf-8"), ]); command } } impl JsonSchema for Cluster { fn schema_name() -> std::borrow::Cow<'static, str> { "Cluster".into() } fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { OptionalClusterDeserialize::json_schema(generator) } }