This commit is contained in:
2026-03-05 04:07:53 +01:00
parent 08c1d0c605
commit 99d343b343
72 changed files with 3235 additions and 3684 deletions

3
crete/.cargo/config.toml Normal file
View File

@@ -0,0 +1,3 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

1
crete/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

1993
crete/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
crete/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "crete"
version = "0.1.0"
edition = "2024"
default-run = "crete"
[dependencies]
minijinja = { version = "2.17.1", features = ["json", "loader"] }
optional_struct = "0.5.2"
regress = "0.10.5"
repo_path_lib = "1.2.4"
reqwest = { version = "0.13.2", features = ["blocking"] }
schemars = { version = "1.2.1", features = ["semver1"] }
semver = { version = "1.0.27", features = ["serde"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
serde_yaml = "0.9.34"
walkdir = "2.5.0"

23
crete/src/bin/schemas.rs Normal file
View File

@@ -0,0 +1,23 @@
use std::{fs::File, io::Write};
use crete::{cluster::Cluster, node::OptionalNode};
use repo_path_lib::repo_dir;
use schemars::{JsonSchema, schema_for};
fn write<T>(name: &str)
where
T: JsonSchema,
{
let mut path = repo_dir().join("schemas").join(name);
path.add_extension("json");
let mut file = File::create(path).unwrap();
let schema = serde_json::to_string_pretty(&schema_for!(T)).unwrap();
file.write_all(schema.as_bytes()).unwrap();
}
// TODO: Create directory if it does not exist
fn main() {
write::<Cluster>("cluster");
write::<OptionalNode>("node");
}

70
crete/src/cluster.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::net::Ipv4Addr;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
base_dir,
node::{Node, OptionalNode},
patch::{PatchEnv, Patches},
};
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Base {
#[serde(default)]
pub(crate) kernel_args: Vec<String>,
#[serde(default)]
pub(crate) patches: Patches,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Version {
kubernetes: semver::Version,
talos: semver::Version,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum ClusterEnv {
Production,
Staging,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Cluster {
#[serde(skip_deserializing)]
name: String,
version: Version,
nodes: Vec<String>,
cluster_env: ClusterEnv,
control_plane_ip: Ipv4Addr,
secrets_file: String,
#[serde(default, skip_serializing)]
pub(crate) default: OptionalNode,
#[serde(skip_serializing)]
pub(crate) base: Base,
// pub secrets_file: PathBuf,
}
impl Cluster {
pub fn get(cluster_name: &str) -> Self {
let mut path = base_dir().join("clusters").join(cluster_name);
path.add_extension("yaml");
let content = std::fs::read_to_string(path).unwrap();
let mut cluster: Self = serde_yaml::from_str(&content).unwrap();
cluster.name = cluster_name.to_string();
cluster
}
pub fn nodes(&self, env: PatchEnv) -> Vec<Node> {
self.nodes
.iter()
.map(|node_name| Node::get(node_name, &env, self))
.collect()
}
}

13
crete/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod cluster;
pub mod node;
pub mod patch;
pub mod schematic;
pub mod secret;
use std::path::PathBuf;
use repo_path_lib::repo_dir;
fn base_dir() -> PathBuf {
repo_dir().join("talos")
}

38
crete/src/main.rs Normal file
View File

@@ -0,0 +1,38 @@
use crete::{cluster::Cluster, patch::PatchEnv};
use minijinja::{Environment, context, path_loader};
use repo_path_lib::repo_dir;
use walkdir::WalkDir;
fn main() {
let cluster = Cluster::get("testing");
let patches = PatchEnv::default();
let nodes = cluster.nodes(patches);
let mut env = Environment::new();
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
let path = repo_dir().join("templates");
env.set_loader(path_loader(&path));
for entry in WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.metadata().unwrap().is_file())
{
let name = entry.path().strip_prefix(&path).unwrap().to_str().unwrap();
let template = env.get_template(name).unwrap();
let result = template
.render(context! {
cluster,
nodes,
root => repo_dir()
})
.unwrap();
println!("###### {name} ######");
println!("{result}");
}
}

139
crete/src/node.rs Normal file
View File

@@ -0,0 +1,139 @@
use std::net::Ipv4Addr;
use optional_struct::{Applicable, optional_struct};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
base_dir,
cluster::Cluster,
patch::{OptionalPatches, PatchEnv, Patches},
schematic::Schematic,
secret::Secret,
};
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
enum NodeType {
Worker,
#[serde(rename(serialize = "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<String>,
}
#[optional_struct]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[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)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Install {
auto: bool,
disk: String,
serial: Option<String>,
}
#[optional_struct]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Node {
#[serde(skip_deserializing)]
hostname: String,
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<String>,
#[optional_rename(OptionalPatches)]
#[optional_wrap]
pub(crate) patches: Patches,
sops: Secret,
}
impl Node {
pub fn get(node_name: &str, env: &PatchEnv, cluster: &Cluster) -> Self {
let mut path = base_dir().join("nodes").join(node_name);
let named = OptionalNode {
hostname: Some(
path.file_name()
.expect("Path should be valid")
.to_string_lossy()
.to_string(),
),
..OptionalNode::default()
};
path.add_extension("yaml");
let content = std::fs::read_to_string(path).unwrap();
let node: OptionalNode = 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 = OptionalNode {
patches: Some(OptionalPatches {
all: Some(vec![]),
control_plane: Some(vec![]),
}),
kernel_args: vec![].into(),
..Default::default()
};
// Combine all the optional node parts into complete struct
let mut node: Node = default
// Apply cluster default settings
.apply(cluster.default.clone())
// Apply hostname based on filename
.apply(named)
// Override node specific settings
.apply(node)
.try_into()
.unwrap();
// Prepend the cluster base values
let mut kernel_args = cluster.base.kernel_args.clone();
kernel_args.extend(node.kernel_args);
node.kernel_args = kernel_args;
let patches = cluster.base.patches.clone().extend(node.patches);
node.patches = patches;
// Render patches
node.patches = node.patches.clone().render(env, cluster, &node);
node
}
}

99
crete/src/patch.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::{net::Ipv4Addr, str::FromStr};
use minijinja::{AutoEscape, Environment, context, path_loader};
use optional_struct::optional_struct;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{base_dir, cluster::Cluster, node::Node};
pub struct PatchEnv<'e>(Environment<'e>);
impl<'e> Default for PatchEnv<'e> {
fn default() -> Self {
let mut env = Environment::new();
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
env.set_loader(path_loader(base_dir().join("patches")));
env.set_auto_escape_callback(|_| AutoEscape::None);
env.add_filter("to_prefix", |netmask: String| {
let netmask = Ipv4Addr::from_str(&netmask).map_err(|err| {
minijinja::Error::new(minijinja::ErrorKind::InvalidOperation, err.to_string())
})?;
let mask = netmask.to_bits();
let prefix = mask.leading_ones();
if mask.checked_shl(prefix).unwrap_or(0) == 0 {
Ok(prefix as u8)
} else {
Err(minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
"invalid IP prefix length",
))
}
});
Self(env)
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub(crate) enum Patch {
Name(String),
#[serde(skip_deserializing)]
#[schemars(with = "serde_json::Value")]
Resolved(serde_yaml::Value),
}
#[optional_struct]
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Patches {
pub(crate) all: Vec<Patch>,
pub(crate) control_plane: Vec<Patch>,
}
fn render(patches: Vec<Patch>, env: &PatchEnv, cluster: &Cluster, node: &Node) -> Vec<Patch> {
patches
.into_iter()
.map(|patch| {
if let Patch::Name(name) = patch {
let content = env
.0
.get_template(&name)
.unwrap()
.render(context! {
node,
cluster
})
.unwrap();
Patch::Resolved(serde_yaml::from_str(&content).unwrap())
} else {
patch
}
})
.collect()
}
impl Patches {
pub(crate) fn extend(mut self, other: Self) -> Self {
self.all.extend(other.all);
self.control_plane.extend(other.control_plane);
Self {
all: self.all,
control_plane: self.control_plane,
}
}
pub(crate) fn render(self, env: &PatchEnv, cluster: &Cluster, node: &Node) -> Self {
Self {
all: render(self.all.clone(), env, cluster, node),
control_plane: render(self.control_plane.clone(), env, cluster, node),
}
}
}

33
crete/src/schematic.rs Normal file
View File

@@ -0,0 +1,33 @@
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use crate::base_dir;
fn deserialize_schematic<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let name: String = Deserialize::deserialize(deserializer)?;
let path = base_dir().join("schematics").join(name);
let content = std::fs::read_to_string(path).unwrap().trim().to_owned();
let client = reqwest::blocking::Client::new();
let res = client
.post("https://factory.talos.dev/schematics")
.body(content)
.send()
.map_err(serde::de::Error::custom)?;
#[derive(Debug, Deserialize)]
struct Response {
id: String,
}
let response: Response = serde_json::from_str(&res.text().map_err(serde::de::Error::custom)?)
.map_err(serde::de::Error::custom)?;
Ok(response.id)
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)]
pub(crate) struct Schematic(#[serde(deserialize_with = "deserialize_schematic")] String);

34
crete/src/secret.rs Normal file
View File

@@ -0,0 +1,34 @@
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use crate::base_dir;
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", untagged)]
enum SecretHelper {
String(String),
File { file: String },
}
pub(crate) fn deserialize_secret<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let secret: SecretHelper = Deserialize::deserialize(deserializer)?;
let value = match secret {
SecretHelper::String(value) => value,
SecretHelper::File { file } => {
let path = base_dir().join("secrets").join(file);
std::fs::read_to_string(path).unwrap().trim().to_owned()
}
};
Ok(value)
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)]
pub(crate) struct Secret(
#[serde(deserialize_with = "deserialize_secret")]
#[schemars(with = "SecretHelper")]
String,
);