feat: Initial rewrite of python render tool
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.yaml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
default_install_hook_types:
|
||||||
|
- pre-commit
|
||||||
|
- commit-msg
|
||||||
|
|
||||||
|
default_stages:
|
||||||
|
- pre-commit
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
glob:
|
||||||
|
- "schemas/*.json"
|
||||||
|
- "CHANGELOG.md"
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: meta
|
||||||
|
hooks:
|
||||||
|
- id: check-hooks-apply
|
||||||
|
- id: check-useless-excludes
|
||||||
|
|
||||||
|
- repo: builtin
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-merge-conflict
|
||||||
|
# - id: check-executables-have-shebangs
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: fmt
|
||||||
|
name: fmt
|
||||||
|
description: Format files with cargo fmt.
|
||||||
|
entry: cargo fmt
|
||||||
|
language: system
|
||||||
|
types: [rust]
|
||||||
|
args: ["--", "--check"]
|
||||||
|
# For some reason some formatting is different depending on how you invoke?
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: clippy
|
||||||
|
name: clippy
|
||||||
|
description: Lint rust sources
|
||||||
|
entry: cargo clippy
|
||||||
|
language: system
|
||||||
|
args: ["--", "-D", "warnings"]
|
||||||
|
types: [file]
|
||||||
|
files: (\.rs|Cargo.lock)$
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: audit
|
||||||
|
name: audit
|
||||||
|
description: Audit packages
|
||||||
|
entry: cargo audit
|
||||||
|
args: ["--deny", "warnings"]
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
|
||||||
|
- id: test
|
||||||
|
name: test
|
||||||
|
description: Rust test
|
||||||
|
entry: cargo test
|
||||||
|
args: ["--workspace"]
|
||||||
|
language: system
|
||||||
|
types: [file]
|
||||||
|
files: (\.rs|Cargo.lock)$
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: xtask
|
||||||
|
name: xtask
|
||||||
|
description: Rust cargo xtask
|
||||||
|
entry: cargo xtask
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v3.1.0
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
|
||||||
|
- repo: https://github.com/crate-ci/typos
|
||||||
|
rev: v1.40.0
|
||||||
|
hooks:
|
||||||
|
- id: typos
|
||||||
|
|
||||||
|
- repo: https://github.com/sirwart/ripsecrets
|
||||||
|
rev: v0.1.11
|
||||||
|
hooks:
|
||||||
|
- id: ripsecrets-system
|
||||||
|
|
||||||
|
- repo: https://github.com/crate-ci/committed
|
||||||
|
rev: v1.1.8
|
||||||
|
hooks:
|
||||||
|
- id: committed
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
imports_granularity = "Module"
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Changelog
|
||||||
Generated
+2893
File diff suppressed because it is too large
Load Diff
+26
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "crete"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
default-run = "crete"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["xtask"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
schemars = { version = "1.2.1", features = ["semver1"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.6.0", features = ["derive"] }
|
||||||
|
clap_complete = "4.6.0"
|
||||||
|
gix-discover = "0.48.0"
|
||||||
|
minijinja = { version = "2.18.0", features = ["json", "loader"] }
|
||||||
|
optional_struct = "0.5.2"
|
||||||
|
reqwest = { version = "0.13.2", features = ["blocking"] }
|
||||||
|
schemars = { workspace = true }
|
||||||
|
semver = { version = "1.0.27", features = ["serde"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
walkdir = "2.5.0"
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
[changelog]
|
||||||
|
header = """
|
||||||
|
# Changelog
|
||||||
|
"""
|
||||||
|
|
||||||
|
[bump]
|
||||||
|
# TODO: Remove this option once we release v1.0.0
|
||||||
|
breaking_always_bump_major = false
|
||||||
|
|
||||||
|
[git]
|
||||||
|
fail_on_unmatched_commit = true
|
||||||
|
require_conventional = true
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "<!-- 0 -->Features" },
|
||||||
|
{ message = "^fix", group = "<!-- 1 -->Bug Fixes" },
|
||||||
|
{ message = "^refactor", group = "<!-- 2 -->Refactor" },
|
||||||
|
{ message = "^test", group = "<!-- 3 -->Testing" },
|
||||||
|
{ message = "^chore\\(release\\): Prepare for", skip = true },
|
||||||
|
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||||
|
{ message = "^chore|^ci", group = "<!-- 4 -->Miscellaneous Tasks" },
|
||||||
|
{ message = "^revert", group = "<!-- 5 -->Revert" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
style = "conventional"
|
||||||
|
|
||||||
|
allowed_types = ["feat", "fix", "refactor", "test", "chore", "revert"]
|
||||||
|
|
||||||
|
# We use rebase merge so there should never be any merge commits
|
||||||
|
merge_commit = false
|
||||||
|
|
||||||
|
# We want to allow fixup and wip commits during development
|
||||||
|
# In the merge train and default branch these should be overridden to disallow it
|
||||||
|
no_fixup = false
|
||||||
|
no_wip = false
|
||||||
|
|
||||||
|
# Remote column length limits as they are annoying
|
||||||
|
subject_length = 0
|
||||||
|
line_length = 0
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly-2026-03-12"
|
||||||
|
profile = "default"
|
||||||
|
components = ["rust-analyzer"]
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "OptionalCluster",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"base": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalBase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"writeOnly": true
|
||||||
|
},
|
||||||
|
"clusterEnv": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ClusterEnv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"controlPlaneIp": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/$defs/OptionalNode",
|
||||||
|
"writeOnly": true
|
||||||
|
},
|
||||||
|
"nodes": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/NodeEntry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"secretsFile": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalVersion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"ClusterEnv": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"production",
|
||||||
|
"staging"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"NodeArch": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"amd64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"NodeEntry": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"NodeType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"worker",
|
||||||
|
"controlPlane"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OptionalBase": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kernelArgs": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patches": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Patches"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalInstall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"auto": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"disk": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"serial": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalNetwork": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dns": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"gateway": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"interface": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"netmask": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"tailscale": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalTailscale"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalNode": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"arch": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/NodeArch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalInstall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kernelArgs": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalNetwork"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ntp": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"patches": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalPatches"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Schematic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sops": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/NodeType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalPatches": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"all": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Patch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"controlPlane": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalTailscale": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"advertiseRoutes": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authKey": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalVersion": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kubernetes": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/SemVer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"talos": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/SemVer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Patch": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Patches": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"all": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Patch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"controlPlane": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"all",
|
||||||
|
"controlPlane"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Schematic": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Secret": {
|
||||||
|
"$ref": "#/$defs/SecretHelper"
|
||||||
|
},
|
||||||
|
"SecretHelper": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"file"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SemVer": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "OptionalNode",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"arch": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/NodeArch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalInstall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"kernelArgs": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalNetwork"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ntp": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"patches": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalPatches"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Schematic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sops": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/NodeType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"NodeArch": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"amd64"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"NodeType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"worker",
|
||||||
|
"controlPlane"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OptionalInstall": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"auto": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"disk": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"serial": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalNetwork": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dns": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"gateway": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"interface": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"netmask": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "ipv4"
|
||||||
|
},
|
||||||
|
"tailscale": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/OptionalTailscale"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalPatches": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"all": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Patch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"controlPlane": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"OptionalTailscale": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"advertiseRoutes": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authKey": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/Secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Patch": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Schematic": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Secret": {
|
||||||
|
"$ref": "#/$defs/SecretHelper"
|
||||||
|
},
|
||||||
|
"SecretHelper": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"file"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use gix_discover::repository::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(version, about)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub global_opts: GlobalOpts,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_is_repo(path: &str) -> Result<PathBuf, String> {
|
||||||
|
let path = PathBuf::from(path);
|
||||||
|
let (repo, _trust) = gix_discover::upwards(&path).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
let work_dir = match repo {
|
||||||
|
Path::LinkedWorkTree { work_dir, .. } => work_dir,
|
||||||
|
Path::WorkTree(work_dir) => work_dir,
|
||||||
|
Path::Repository(git_dir) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Repo '{}' has no working directory",
|
||||||
|
git_dir.to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(work_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct GlobalOpts {
|
||||||
|
#[arg(global = true, short, long, action = clap::ArgAction::Count)]
|
||||||
|
/// Use verbose output
|
||||||
|
pub verbose: u8,
|
||||||
|
|
||||||
|
#[arg(global = true, short, long, value_parser = path_is_repo, default_value_os_t = PathBuf::from("."))]
|
||||||
|
/// Path to the repo containing the config files
|
||||||
|
pub repo: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Generate talos config file
|
||||||
|
Generate,
|
||||||
|
/// Generate completions for your current shell
|
||||||
|
ShellCompletions,
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
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::get_talos_config_path;
|
||||||
|
use crate::node::{Node, OptionalNode};
|
||||||
|
use crate::patch::Patches;
|
||||||
|
|
||||||
|
#[optional_struct]
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[optional_struct]
|
||||||
|
#[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, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub enum ClusterEnv {
|
||||||
|
Production,
|
||||||
|
Staging,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum NodeEntry {
|
||||||
|
Name(String),
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
Node(Box<Node>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[optional_struct]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub struct Cluster {
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
name: String,
|
||||||
|
#[optional_rename(OptionalVersion)]
|
||||||
|
#[optional_wrap]
|
||||||
|
version: Version,
|
||||||
|
nodes: Vec<NodeEntry>,
|
||||||
|
cluster_env: ClusterEnv,
|
||||||
|
control_plane_ip: Ipv4Addr,
|
||||||
|
secrets_file: PathBuf,
|
||||||
|
#[serde(default, skip_serializing)]
|
||||||
|
#[optional_skip_wrap]
|
||||||
|
pub(crate) default: OptionalNode,
|
||||||
|
#[optional_rename(OptionalBase)]
|
||||||
|
#[optional_wrap]
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub(crate) base: Base,
|
||||||
|
// pub secrets_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cluster {
|
||||||
|
pub fn get(cluster_name: &str, env: &Environment) -> Self {
|
||||||
|
let path = get_talos_config_path()
|
||||||
|
.join("clusters")
|
||||||
|
.join("default.yaml");
|
||||||
|
|
||||||
|
let default: OptionalCluster = 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_config_path().join("clusters").join(cluster_name);
|
||||||
|
path.add_extension("yaml");
|
||||||
|
let content = std::fs::read_to_string(path).unwrap();
|
||||||
|
|
||||||
|
let mut cluster: OptionalCluster = serde_yaml::from_str(&content).unwrap();
|
||||||
|
cluster.name = Some(cluster_name.to_string());
|
||||||
|
|
||||||
|
// 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 mut cluster: Self = default.apply(cluster).try_into().unwrap();
|
||||||
|
|
||||||
|
cluster.nodes = cluster
|
||||||
|
.nodes
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|node_entry| {
|
||||||
|
if let NodeEntry::Name(name) = node_entry {
|
||||||
|
NodeEntry::Node(Box::new(Node::get(&name, env, &cluster)))
|
||||||
|
} else {
|
||||||
|
node_entry
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
cluster.secrets_file = get_talos_config_path()
|
||||||
|
.join("secrets")
|
||||||
|
.join(cluster.secrets_file)
|
||||||
|
.absolute()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cluster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_clusters(env: &Environment) -> Vec<Cluster> {
|
||||||
|
WalkDir::new(get_talos_config_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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use minijinja::{AutoEscape, Environment, path_loader};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::{get_repo_path, get_talos_config_path};
|
||||||
|
|
||||||
|
/// Transparent wrapper around minijinja::Environment that loads templates from a path and
|
||||||
|
/// configures better defaults. It also implements IntoIter, making it possible to iterate over all
|
||||||
|
/// the templates.
|
||||||
|
pub struct PathEnvironment<'a> {
|
||||||
|
env: Environment<'a>,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PathEnvironment<'a> {
|
||||||
|
pub fn new(path: &Path) -> Self {
|
||||||
|
let mut env = Environment::new();
|
||||||
|
|
||||||
|
// Configure jinja
|
||||||
|
env.set_trim_blocks(true);
|
||||||
|
env.set_lstrip_blocks(true);
|
||||||
|
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
|
||||||
|
env.set_auto_escape_callback(|_| AutoEscape::None);
|
||||||
|
|
||||||
|
// Add filters
|
||||||
|
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",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add path loader
|
||||||
|
env.set_loader(path_loader(&path));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
path: path.into(),
|
||||||
|
env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_patches() -> Self {
|
||||||
|
Self::new(&get_talos_config_path().join("patches"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_templates(repo: &Path) -> Self {
|
||||||
|
let mut env = Self::new(&get_repo_path().join("templates"));
|
||||||
|
|
||||||
|
let path = repo.absolute().unwrap();
|
||||||
|
env.env.add_filter("kubeconfig", move |names: Vec<String>| {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.map(|name| {
|
||||||
|
path.join("configs")
|
||||||
|
.join(name)
|
||||||
|
.join("kubeconfig")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make PathEnvironment act like a normal Environment transparently
|
||||||
|
impl<'a> Deref for PathEnvironment<'a> {
|
||||||
|
type Target = Environment<'a>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over all the files in the path
|
||||||
|
impl<'a, 'b> IntoIterator for &'b PathEnvironment<'a> {
|
||||||
|
type Item = String;
|
||||||
|
|
||||||
|
type IntoIter = impl Iterator<Item = Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
// Find all templates in path
|
||||||
|
WalkDir::new(&self.path)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| e.metadata().unwrap().is_file())
|
||||||
|
.map(|e| {
|
||||||
|
e.path()
|
||||||
|
.strip_prefix(&self.path)
|
||||||
|
.expect("All paths should have prefix")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#![feature(impl_trait_in_assoc_type, path_absolute_method)]
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
pub mod cluster;
|
||||||
|
pub mod environment;
|
||||||
|
pub mod node;
|
||||||
|
pub mod patch;
|
||||||
|
pub mod schematic;
|
||||||
|
pub mod secret;
|
||||||
|
|
||||||
|
pub(crate) static REPO_PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn set_repo_path(path: impl Into<PathBuf>) {
|
||||||
|
REPO_PATH
|
||||||
|
.set(path.into())
|
||||||
|
.expect("Repo path already initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_repo_path() -> &'static Path {
|
||||||
|
REPO_PATH.get().expect("Repo path not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_talos_config_path() -> PathBuf {
|
||||||
|
get_repo_path().join("talos")
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
mod cli;
|
||||||
|
|
||||||
|
use clap::{CommandFactory, Parser};
|
||||||
|
use clap_complete::{Shell, generate as generate_complete};
|
||||||
|
use crete::cluster::get_clusters;
|
||||||
|
use crete::environment::PathEnvironment;
|
||||||
|
use crete::set_repo_path;
|
||||||
|
use minijinja::context;
|
||||||
|
|
||||||
|
use crate::cli::{Cli, Commands, GlobalOpts};
|
||||||
|
|
||||||
|
fn generate(opts: &GlobalOpts) {
|
||||||
|
set_repo_path(&opts.repo);
|
||||||
|
|
||||||
|
let patch_env = PathEnvironment::new_patches();
|
||||||
|
let clusters = get_clusters(&patch_env);
|
||||||
|
|
||||||
|
let path = opts.repo.join("rendered");
|
||||||
|
if path.exists() {
|
||||||
|
std::fs::remove_dir_all(&path).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::create_dir(&path).unwrap();
|
||||||
|
|
||||||
|
let template_env = PathEnvironment::new_templates(&opts.repo);
|
||||||
|
for template_name in &template_env {
|
||||||
|
let template = template_env.get_template(&template_name).unwrap();
|
||||||
|
|
||||||
|
let content = template
|
||||||
|
.render(context! {
|
||||||
|
clusters,
|
||||||
|
root => opts.repo
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
std::fs::write(path.join(template_name), content).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Generate => generate(&cli.global_opts),
|
||||||
|
Commands::ShellCompletions => generate_complete(
|
||||||
|
Shell::from_env().unwrap_or(Shell::Bash),
|
||||||
|
&mut Cli::command(),
|
||||||
|
"crete",
|
||||||
|
&mut std::io::stdout(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
use minijinja::Environment;
|
||||||
|
use optional_struct::{Applicable, optional_struct};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::cluster::Cluster;
|
||||||
|
use crate::get_talos_config_path;
|
||||||
|
use crate::patch::{OptionalPatches, Patches};
|
||||||
|
use crate::schematic::Schematic;
|
||||||
|
use crate::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, 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[optional_struct]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq)]
|
||||||
|
#[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: &Environment, cluster: &Cluster) -> Self {
|
||||||
|
let mut path = get_talos_config_path().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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use minijinja::{Environment, context};
|
||||||
|
use optional_struct::optional_struct;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::cluster::Cluster;
|
||||||
|
use crate::node::Node;
|
||||||
|
|
||||||
|
#[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: &Environment, cluster: &Cluster, node: &Node) -> Vec<Patch> {
|
||||||
|
patches
|
||||||
|
.into_iter()
|
||||||
|
.map(|patch| {
|
||||||
|
if let Patch::Name(name) = patch {
|
||||||
|
let content = env
|
||||||
|
.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: &Environment, cluster: &Cluster, node: &Node) -> Self {
|
||||||
|
Self {
|
||||||
|
all: render(self.all.clone(), env, cluster, node),
|
||||||
|
control_plane: render(self.control_plane.clone(), env, cluster, node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
use crate::get_talos_config_path;
|
||||||
|
|
||||||
|
fn deserialize_schematic<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let name: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
let path = get_talos_config_path().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);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
use crate::get_talos_config_path;
|
||||||
|
|
||||||
|
#[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 = get_talos_config_path().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,
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
repo_path = "1.2.4"
|
||||||
|
schemars = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
crete = { path = ".." }
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crete::cluster::OptionalCluster;
|
||||||
|
use crete::node::OptionalNode;
|
||||||
|
use repo_path::repo_path;
|
||||||
|
use schemars::{JsonSchema, schema_for};
|
||||||
|
|
||||||
|
fn write<T>(name: &str)
|
||||||
|
where
|
||||||
|
T: JsonSchema,
|
||||||
|
{
|
||||||
|
let mut path = repo_path!("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();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
write::<OptionalCluster>("cluster");
|
||||||
|
write::<OptionalNode>("node");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user