use std::net::Ipv4Addr; use std::process::Command; use minijinja::Environment; use optional_struct::{Applicable, optional_struct}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::cluster::{Base, Cluster}; use crate::patch::{OptionalPatches, OptionalPatchesString, Patches}; use crate::schematic::Schematic; use crate::secret::Secret; use crate::{get_configs_path, get_talos_path}; #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum NodeType { Worker, #[serde(rename(serialize = "controlplane"))] ControlPlane, } impl From for &str { fn from(value: NodeType) -> Self { match value { NodeType::Worker => "worker", NodeType::ControlPlane => "controlplane", } } } #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum NodeArch { Amd64, } #[optional_struct] #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Tailscale { auth_key: Secret, advertise_routes: bool, #[serde(default)] server: Option, } #[optional_struct] #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Network { interface: String, ip: Ipv4Addr, netmask: Ipv4Addr, gateway: Ipv4Addr, dns: [Ipv4Addr; 2], #[optional_rename(OptionalTailscale)] #[optional_wrap] tailscale: Tailscale, } #[optional_struct] #[derive(Debug, Serialize, Deserialize, JsonSchema, Default, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct Install { auto: bool, disk: String, serial: Option, } #[optional_struct] #[derive(Debug, Deserialize, JsonSchema, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub(crate) struct NodeDeserialize { arch: NodeArch, schematic: Schematic, r#type: NodeType, #[optional_rename(OptionalNetwork)] #[optional_wrap] network: Network, ntp: String, #[optional_rename(OptionalInstall)] #[optional_wrap] install: Install, kernel_args: Vec, #[optional_rename(OptionalPatchesString)] #[optional_wrap] patches: Patches, sops: Secret, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Node { hostname: String, arch: NodeArch, schematic: Schematic, r#type: NodeType, network: Network, ntp: String, install: Install, kernel_args: Vec, patches: Patches, sops: Secret, } impl Node { pub(crate) fn get( node_name: &str, env: &Environment, cluster: &Cluster, default: &OptionalNodeDeserialize, base: &Base, ) -> Self { let mut path = get_talos_path().join("nodes").join(node_name); let hostname = path .file_name() .expect("Path should be valid") .to_string_lossy() .to_string(); path.add_extension("yaml"); let content = std::fs::read_to_string(path).unwrap(); let node: OptionalNodeDeserialize = serde_yaml::from_str(&content).unwrap(); // We want all vectors to be empty vectors by default // Sadly we have to this manually // TODO: Find a better way of doing this let default = OptionalNodeDeserialize { patches: Some(OptionalPatches { all: Some(vec![]), control_plane: Some(vec![]), }), kernel_args: vec![].into(), ..Default::default() } .apply(default.clone()); // Combine all the optional node parts into complete struct let node: NodeDeserialize = default // Override node specific settings .apply(node) .try_into() .unwrap(); // Prepend the cluster base values let mut kernel_args = base.kernel_args.clone(); kernel_args.extend(node.kernel_args); let patches = base.patches.clone().extend(node.patches); let node = Node { hostname, arch: node.arch, schematic: node.schematic, r#type: node.r#type, network: node.network, ntp: node.ntp, install: node.install, kernel_args, patches: Default::default(), sops: node.sops, }; // Render patches let patches = patches.render(env, cluster, &node); Node { patches, ..node } } pub fn talosctl_gen_config_command(&self, cluster: &Cluster) -> Command { let mut path = get_configs_path().join(&cluster.name).join(&self.hostname); path.add_extension("yaml"); let mut command = Command::new("talosctl"); command.args([ "gen", "config", &cluster.name, &format!("https://{}:6443", cluster.control_plane_ip), "--with-secrets", cluster.secrets_file.to_str().expect("Path should be valid"), "--talos-version", &cluster.version.talos(), "--kubernetes-version", &cluster.version.kubernetes(), "--output-types", self.r#type.into(), "--install-image", &format!( "factory.talos.dev/metal-installer/{}:{}", self.schematic, cluster.version.talos() ), "--with-docs=false", "--with-examples=false", "-o", path.to_str().expect("Path should be valid utf-8"), ]); for patch in &self.patches.all { command.args(["--config-patch", &serde_json::to_string(&patch).unwrap()]); } for patch in &self.patches.control_plane { command.args([ "--config-patch-control-plane", &serde_json::to_string(&patch).unwrap(), ]); } command } } impl JsonSchema for Node { fn schema_name() -> std::borrow::Cow<'static, str> { "Node".into() } fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { OptionalNodeDeserialize::json_schema(generator) } }