Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ jobs:
pip install pyyaml --quiet
python3 scripts/validate.py

- name: Check for version drift against versions.yaml
run: |
python3 scripts/check-version-drift.py --selftest
python3 scripts/check-version-drift.py

pluto:
name: API deprecation check
runs-on: ubuntu-latest
Expand All @@ -70,7 +75,14 @@ jobs:
- name: Read target Kubernetes version
id: k8s
run: |
K8S_VERSION=$(grep '^kubernetes_version:' omni/talos-image.yaml | awk '{print $2}')
# Desired version, from versions.yaml — NOT talos-image.yaml, which
# is the build ledger and records what was last built. Reading the
# ledger here checked deprecations against a version no cluster runs.
K8S_VERSION=$(grep '^kubernetes:' omni/versions.yaml | awk '{print $2}')
if [[ -z "$K8S_VERSION" ]]; then
echo "ERROR: no kubernetes pin in omni/versions.yaml" >&2
exit 1
fi
echo "version=${K8S_VERSION}" >> "$GITHUB_OUTPUT"
echo "Checking against Kubernetes ${K8S_VERSION}"

Expand Down
41 changes: 0 additions & 41 deletions .github/workflows/talos-image-update.yml

This file was deleted.

2 changes: 1 addition & 1 deletion omni/machine-classes/control-plane.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ metadata:
type: MachineClasses.omni.sidero.dev
id: oci-cp
spec:
installImage: ghcr.io/syscode-labs/talos-images/installer:v1.14.0-beta.1
installImage: ghcr.io/syscode-labs/talos-images/installer:v1.13.7
matchLabels:
# Synced into Omni; UI can select this class after machines join.
# Labels may be assigned by automation or, for lab recovery, in Omni UI.
Expand Down
19 changes: 0 additions & 19 deletions omni/machine-classes/unraid-control-plane.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion omni/machine-classes/worker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ metadata:
type: MachineClasses.omni.sidero.dev
id: oci-worker
spec:
installImage: ghcr.io/syscode-labs/talos-images/installer:v1.14.0-beta.1
installImage: ghcr.io/syscode-labs/talos-images/installer:v1.13.7
matchLabels:
# Synced into Omni; UI can select this class after machines join.
# Labels may be assigned by automation or, for lab recovery, in Omni UI.
Expand Down
24 changes: 24 additions & 0 deletions omni/versions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
# Desired Talos / Kubernetes versions. HAND-OWNED — edit this file to upgrade.
#
# Build outputs (schematic_id, oci_image_ocid, installer and extension image
# refs) live in talos-image.yaml, which is rewritten wholesale by the
# talos-images build workflow. Never put desired versions there: that file
# records what has been built, not what is intended to run.
#
# Clusters inherit the global pin. A cluster entry overrides it.
#
# Kubernetes is never inherited across a diverging Talos target: a cluster that
# overrides `talos` without `kubernetes` derives its Kubernetes version from
# the supported versions of its own Talos target, not from the global pin.

talos: v1.13.7
kubernetes: v1.36.3

clusters:
# Sacrificial canary. Always takes a prerelease first; recovery is destroy
# and re-provision at the previous pin, never a Talos downgrade.
unraid-lab: {}

# No etcd restore point yet — promotion is gated on a verified backup.
oci-lab: {}
150 changes: 150 additions & 0 deletions scripts/check-version-drift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Fail if any version-bearing file disagrees with omni/versions.yaml.

versions.yaml is the hand-owned declaration of what should run. Every other
version reference in this repo is derived from it and must agree. Without this
check, editing versions.yaml in a way that needs no new image (flipping a
cluster back to a version already built, say) leaves the derived files stale,
because nothing rebuilds them — the build workflow only opens a PR when it has
an image to build.

That is the failure this repo already lived through: the ledger and machine
classes moved to v1.14.0-beta.1 while oci-lab.yaml stayed on v1.13.7, and
nothing failed until a cluster refused its config.

Runs identically locally and in CI — `python3 scripts/check-version-drift.py`.
"""

import os
import re
import sys

import yaml

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Files owned by a cluster: which cluster, and which versions to check.
# unraid-lab's machine classes and template are generated in omni-on-unraid
# from this same file, so they are deliberately absent here.
OCI_LAB_FILES = [
"omni/cluster-templates/oci-lab.yaml",
"omni/machine-classes/control-plane.yaml",
"omni/machine-classes/worker.yaml",
]


def resolve(versions, cluster):
"""Resolve (talos, kubernetes) for a cluster.

Kubernetes is never inherited across a diverging Talos target: a cluster
that overrides talos without kubernetes cannot keep the global Kubernetes
pin, which belongs to a different Talos minor.
"""
entry = (versions.get("clusters") or {}).get(cluster) or {}
talos = entry.get("talos") or versions.get("talos")
k8s = entry.get("kubernetes")
if k8s is None:
if entry.get("talos") and entry["talos"] != versions.get("talos"):
return talos, None # caller reports: needs an explicit pin
k8s = versions.get("kubernetes")
return talos, k8s


def main():
path = os.path.join(ROOT, "omni/versions.yaml")
with open(path) as fh:
versions = yaml.safe_load(fh) or {}

if not versions.get("talos") or not versions.get("kubernetes"):
print("ERROR: omni/versions.yaml must set both talos and kubernetes")
return 1

talos, k8s = resolve(versions, "oci-lab")
errors = []

if k8s is None:
errors.append(
"oci-lab overrides talos to %s but sets no kubernetes version. "
"Derive it from that Talos release and pin it explicitly — the "
"global kubernetes pin belongs to a different Talos minor." % talos
)
k8s = ""

for rel in OCI_LAB_FILES:
full = os.path.join(ROOT, rel)
if not os.path.exists(full):
errors.append("%s: missing" % rel)
continue
text = open(full).read()

if "machine-classes" in rel:
found = re.findall(r"^\s*installImage:.*:(\S+)$", text, re.M)
if not found:
errors.append("%s: no installImage found" % rel)
for tag in found:
# Tags may carry a variant suffix, e.g. v1.13.7-libvirt.
if not tag.startswith(talos):
errors.append(
"%s: installImage tag %s does not match oci-lab's "
"Talos pin %s" % (rel, tag, talos)
)
else:
doc = yaml.safe_load(text) or {}
got_talos = (doc.get("talos") or {}).get("version")
got_k8s = (doc.get("kubernetes") or {}).get("version")
if got_talos != talos:
errors.append(
"%s: talos.version is %s, versions.yaml says %s"
% (rel, got_talos, talos)
)
if got_k8s != k8s:
errors.append(
"%s: kubernetes.version is %s, versions.yaml says %s"
% (rel, got_k8s, k8s)
)

if errors:
print("Version drift against omni/versions.yaml:\n")
for err in errors:
print(" - %s" % err)
print(
"\nversions.yaml is the source of truth. Update the derived files "
"to match it, or correct the pin."
)
return 1

print("No version drift. oci-lab: Talos %s, Kubernetes %s" % (talos, k8s))
return 0


def selftest():
g = {"talos": "v1.13.7", "kubernetes": "v1.36.3"}

v = dict(g, clusters={"a": {}})
assert resolve(v, "a") == ("v1.13.7", "v1.36.3"), "inherit both"

v = dict(g, clusters={"a": None})
assert resolve(v, "a") == ("v1.13.7", "v1.36.3"), "null entry inherits"

v = dict(g, clusters={})
assert resolve(v, "missing") == ("v1.13.7", "v1.36.3"), "absent cluster inherits"

# Diverging Talos must not silently keep the global Kubernetes pin.
v = dict(g, clusters={"a": {"talos": "v1.14.0"}})
assert resolve(v, "a") == ("v1.14.0", None), "diverged talos needs explicit k8s"

v = dict(g, clusters={"a": {"talos": "v1.14.0", "kubernetes": "v1.37.0"}})
assert resolve(v, "a") == ("v1.14.0", "v1.37.0"), "both overridden"

# Restating the global pin is not divergence.
v = dict(g, clusters={"a": {"talos": "v1.13.7"}})
assert resolve(v, "a") == ("v1.13.7", "v1.36.3"), "redundant pin still inherits"

print("selftest ok")
return 0


if __name__ == "__main__":
if "--selftest" in sys.argv:
sys.exit(selftest())
sys.exit(main())
Loading