use std::net::Ipv4Addr; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; use minijinja::Environment; use optional_struct::{Applicable, optional_struct}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use walkdir::WalkDir; 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)] #[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) fn get(cluster_name: &str, env: &Environment) -> Self { let path = get_talos_path().join("clusters").join("default.yaml"); let default: OptionalClusterDeserialize = if path.exists() { let content = std::fs::read_to_string(path).unwrap(); serde_yaml::from_str(&content).unwrap() } else { Default::default() }; let mut path = get_talos_path().join("clusters").join(cluster_name); path.add_extension("yaml"); let content = std::fs::read_to_string(path).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| Node::get(name, env, &cluster, &default, &base)) .collect(); Self { nodes, ..cluster } } pub fn get_all() -> Vec { let env = PathEnvironment::new_patches(); WalkDir::new(get_talos_path().join("clusters")) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.metadata().unwrap().is_file()) .filter_map(|e| { 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(Cluster::get(&name, &env)) }) .collect() } 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) } }