···11+#| Place user jinja template overrides in this file's directory |#
22+#| Docs: https://mirkolenz.github.io/makejinja/makejinja.html |#
33+#| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/makejinja.toml |#
44+#| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input1/not-empty.yaml.jinja |#
55+#| Example: https://github.com/mirkolenz/makejinja/blob/main/tests/data/input2/not-empty.yaml.jinja |#
+81
bootstrap/scripts/plugin.py
···11+import importlib.util
22+import sys
33+from collections.abc import Callable
44+from pathlib import Path
55+from typing import Any
66+77+from typing import Any
88+from netaddr import IPNetwork
99+1010+import makejinja
1111+import validation
1212+1313+1414+# Return the filename of a path without the j2 extension
1515+def basename(value: str) -> str:
1616+ return Path(value).stem
1717+1818+1919+# Return a list of files in the talos patches directory
2020+def talos_patches(value: str) -> list[str]:
2121+ path = Path(f'bootstrap/templates/kubernetes/bootstrap/talos/patches/{value}')
2222+ if not path.is_dir():
2323+ return []
2424+ return [str(f) for f in sorted(path.glob('*.yaml.j2')) if f.is_file()]
2525+2626+2727+# Return the nth host in a CIDR range
2828+def nthhost(value: str, query: int) -> str:
2929+ value = IPNetwork(value)
3030+ try:
3131+ nth = int(query)
3232+ if value.size > nth:
3333+ return str(value[nth])
3434+ except ValueError:
3535+ return False
3636+ return value
3737+3838+3939+def import_filter(file: Path) -> Callable[[dict[str, Any]], bool]:
4040+ module_path = file.relative_to(Path.cwd()).with_suffix("")
4141+ module_name = str(module_path).replace("/", ".")
4242+ spec = importlib.util.spec_from_file_location(module_name, file)
4343+ assert spec is not None
4444+ module = importlib.util.module_from_spec(spec)
4545+ sys.modules[module_name] = module
4646+ assert spec.loader is not None
4747+ spec.loader.exec_module(module)
4848+ return module.main
4949+5050+5151+class Plugin(makejinja.plugin.Plugin):
5252+ def __init__(self, data: dict[str, Any], config: makejinja.config.Config):
5353+ self._data = data
5454+ self._config = config
5555+5656+ self._excluded_dirs: set[Path] = set()
5757+ for input_path in config.inputs:
5858+ for filter_file in input_path.rglob(".mjfilter.py"):
5959+ filter_func = import_filter(filter_file)
6060+ if filter_func(data) is False:
6161+ self._excluded_dirs.add(filter_file.parent)
6262+6363+ validation.validate(data)
6464+6565+6666+ def filters(self) -> makejinja.plugin.Filters:
6767+ return [basename, nthhost]
6868+6969+7070+ def functions(self) -> makejinja.plugin.Functions:
7171+ return [talos_patches]
7272+7373+7474+ def path_filters(self):
7575+ return [self._mjfilter_func]
7676+7777+7878+ def _mjfilter_func(self, path: Path) -> bool:
7979+ return not any(
8080+ path.is_relative_to(excluded_dir) for excluded_dir in self._excluded_dirs
8181+ )
+113
bootstrap/scripts/validation.py
···11+from functools import wraps
22+from shutil import which
33+from typing import Callable, cast
44+from zoneinfo import available_timezones
55+import netaddr
66+import re
77+import socket
88+import sys
99+1010+GLOBAL_CLI_TOOLS = ["age", "flux", "helmfile", "sops", "jq", "kubeconform", "kustomize", "talosctl", "talhelper"]
1111+CLOUDFLARE_TOOLS = ["cloudflared"]
1212+1313+1414+def required(*keys: str):
1515+ def wrapper_outter(func: Callable):
1616+ @wraps(func)
1717+ def wrapper(data: dict, *_, **kwargs) -> None:
1818+ for key in keys:
1919+ if data.get(key) is None:
2020+ raise ValueError(f"Missing required key {key}")
2121+ return func(*[data[key] for key in keys], **kwargs)
2222+2323+ return wrapper
2424+2525+ return wrapper_outter
2626+2727+2828+def validate_python_version() -> None:
2929+ required_version = (3, 11, 0)
3030+ if sys.version_info < required_version:
3131+ raise ValueError(f"Python {sys.version_info} is below 3.11. Please upgrade.")
3232+3333+3434+def validate_ip(ip: str) -> str:
3535+ try:
3636+ netaddr.IPAddress(ip)
3737+ except netaddr.core.AddrFormatError as e:
3838+ raise ValueError(f"Invalid IP address {ip}") from e
3939+ return ip
4040+4141+4242+def validate_network(cidr: str, family: int) -> str:
4343+ try:
4444+ network = netaddr.IPNetwork(cidr)
4545+ if network.version != family:
4646+ raise ValueError(f"Invalid CIDR family {network.version}")
4747+ except netaddr.core.AddrFormatError as e:
4848+ raise ValueError(f"Invalid CIDR {cidr}") from e
4949+ return cidr
5050+5151+5252+def validate_node(node: dict, node_cidr: str) -> None:
5353+ if not node.get("name"):
5454+ raise ValueError(f"A node is missing a name")
5555+ if not re.match(r"^[a-z0-9-]+$", node.get('name')):
5656+ raise ValueError(f"Node {node.get('name')} has an invalid name")
5757+ if not node.get("disk"):
5858+ raise ValueError(f"Node {node.get('name')} is missing disk")
5959+ if not node.get("mac_addr"):
6060+ raise ValueError(f"Node {node.get('name')} is missing mac_addr")
6161+ if not re.match(r"(?:[0-9a-fA-F]:?){12}", node.get("mac_addr")):
6262+ raise ValueError(f"Node {node.get('name')} has an invalid mac_addr, is this a MAC address?")
6363+ if node.get("address"):
6464+ ip = validate_ip(node.get("address"))
6565+ if netaddr.IPAddress(ip, 4) not in netaddr.IPNetwork(node_cidr):
6666+ raise ValueError(f"Node {node.get('name')} is not in the node CIDR {node_cidr}")
6767+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
6868+ sock.settimeout(5)
6969+ result = sock.connect_ex((ip, 50000))
7070+ if result != 0:
7171+ raise ValueError(f"Node {node.get('name')} port 50000 is not open")
7272+7373+7474+@required("bootstrap_cloudflare")
7575+def validate_cli_tools(cloudflare: dict, **_) -> None:
7676+ for tool in GLOBAL_CLI_TOOLS:
7777+ if not which(tool):
7878+ raise ValueError(f"Missing required CLI tool {tool}")
7979+ for tool in CLOUDFLARE_TOOLS if cloudflare.get("enabled", False) else []:
8080+ if not which(tool):
8181+ raise ValueError(f"Missing required CLI tool {tool}")
8282+8383+8484+@required("bootstrap_sops_age_pubkey")
8585+def validate_age(key: str, **_) -> None:
8686+ if not re.match(r"^age1[a-z0-9]{0,58}$", key):
8787+ raise ValueError(f"Invalid Age public key {key}")
8888+8989+9090+@required("bootstrap_node_network", "bootstrap_node_inventory")
9191+def validate_nodes(node_cidr: str, nodes: dict[list], **_) -> None:
9292+ node_cidr = validate_network(node_cidr, 4)
9393+9494+ controllers = [node for node in nodes if node.get('controller') == True]
9595+ if len(controllers) < 1:
9696+ raise ValueError(f"Must have at least one controller node")
9797+ if len(controllers) % 2 == 0:
9898+ raise ValueError(f"Must have an odd number of controller nodes")
9999+ for node in controllers:
100100+ validate_node(node, node_cidr)
101101+102102+ workers = [node for node in nodes if node.get('controller') == False]
103103+ for node in workers:
104104+ validate_node(node, node_cidr)
105105+106106+107107+def validate(data: dict) -> None:
108108+ validate_python_version()
109109+ validate_cli_tools(data)
110110+ validate_age(data)
111111+112112+ if not data.get("skip_tests", False):
113113+ validate_nodes(data)
···11+#% if ((not bootstrap_bgp.enabled) and (not bootstrap_feature_gates.dual_stack_ipv4_first)) %#
22+---
33+# https://docs.cilium.io/en/latest/network/l2-announcements
44+apiVersion: cilium.io/v2alpha1
55+kind: CiliumL2AnnouncementPolicy
66+metadata:
77+ name: l2-policy
88+spec:
99+ loadBalancerIPs: true
1010+ # NOTE: interfaces might need to be set if you have more than one active NIC on your hosts
1111+ # interfaces:
1212+ # - ^eno[0-9]+
1313+ # - ^eth[0-9]+
1414+ nodeSelector:
1515+ matchLabels:
1616+ kubernetes.io/os: linux
1717+---
1818+apiVersion: cilium.io/v2alpha1
1919+kind: CiliumLoadBalancerIPPool
2020+metadata:
2121+ name: l2-pool
2222+spec:
2323+ allowFirstLastIPs: "Yes"
2424+ blocks:
2525+ - cidr: "#{ bootstrap_node_network }#"
2626+#% endif %#
···11+# Talos Patching
22+33+This directory contains Kustomization patches that are added to the talhelper configuration file.
44+55+<https://www.talos.dev/v1.7/talos-guides/configuration/patching/>
66+77+## Patch Directories
88+99+Under this `patches` directory, there are several sub-directories that can contain patches that are added to the talhelper configuration file.
1010+Each directory is optional and therefore might not created by default.
1111+1212+- `global/`: patches that are applied to both the controller and worker configurations
1313+- `controller/`: patches that are applied to the controller configurations
1414+- `worker/`: patches that are applied to the worker configurations
1515+- `${node-hostname}/`: patches that are applied to the node with the specified name
···11+---
22+apiVersion: v1
33+kind: ConfigMap
44+metadata:
55+ name: cluster-settings
66+ namespace: flux-system
77+data:
88+ SETTING_EXAMPLE: Global settings for your cluster go in this file, this file is NOT encrypted