From 2a6d3d6aeddf5a50d2f87067ea0a488fea3eabb5 Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Wed, 5 Aug 2026 02:22:33 +0100 Subject: [PATCH 1/8] fix(omni): make the oci-lab template and machine classes actually usable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither had ever worked, which is why oci-lab could not allocate machines. 1. oci-lab.yaml nested controlPlane and workers under the Cluster document. omnictl rejects that outright: field controlPlane not found in type models.Cluster field workers not found in type models.Cluster They must be separate --- documents. The template could never be validated, let alone synced, so the cluster was built some other way and the file was decorative. 2. The machine classes wrote matchLabels as a camelCase map. Omni takes matchlabels (lowercase) as a LIST of selector strings and rejects the map with "machine class should either have auto provision or match labels set". So oci-cp and oci-worker did not exist in Omni at all — only the unraid-* classes were registered. Also drops the stale hint to use `omnictl machine set-labels`, which does not exist in omnictl 1.9.3; labels are set with a MachineLabels resource. Verified live: both classes now apply, the template validates, and a sync allocates both OCI machines to oci-lab. --- omni/cluster-templates/oci-lab.yaml | 23 +++++++++++++++-------- omni/machine-classes/control-plane.yaml | 17 ++++++++++++----- omni/machine-classes/worker.yaml | 17 ++++++++++++----- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/omni/cluster-templates/oci-lab.yaml b/omni/cluster-templates/oci-lab.yaml index ba234d0..0943ee2 100644 --- a/omni/cluster-templates/oci-lab.yaml +++ b/omni/cluster-templates/oci-lab.yaml @@ -15,11 +15,18 @@ patches: - file: ../patches/disable-kube-proxy.yaml - file: ../patches/allow-scheduling.yaml - file: ../patches/oci-lab-inline-manifests.yaml -controlPlane: - machineClass: - name: oci-cp - size: 1 -workers: - - machineClass: - name: oci-worker - size: 1 +--- +# omnictl requires ControlPlane and Workers as SEPARATE documents. They were +# nested under the Cluster document, which omnictl rejects outright: +# field controlPlane not found in type models.Cluster +# field workers not found in type models.Cluster +# So this template was never syncable in that form. +kind: ControlPlane +machineClass: + name: oci-cp + size: 1 +--- +kind: Workers +machineClass: + name: oci-worker + size: 1 diff --git a/omni/machine-classes/control-plane.yaml b/omni/machine-classes/control-plane.yaml index 01065cc..8f350d5 100644 --- a/omni/machine-classes/control-plane.yaml +++ b/omni/machine-classes/control-plane.yaml @@ -5,8 +5,15 @@ metadata: id: oci-cp spec: 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. - # omnictl machine set-labels role=oci-cp - role: oci-cp + # Omni's MachineClass spec takes `matchlabels` (lowercase) as a LIST of + # label selector strings. It was written as a camelCase map, which Omni + # rejects with "machine class should either have auto provision or match + # labels set" — so these classes had never been applied and did not exist + # in Omni at all, which is why oci-lab could not allocate machines. + # + # Set the label on a machine with a MachineLabels resource: + # metadata: {namespace: default, type: MachineLabels.omni.sidero.dev, + # id: , labels: {role: oci-cp}} + # (`omnictl machine set-labels` does not exist in omnictl 1.9.3.) + matchlabels: + - role=oci-cp diff --git a/omni/machine-classes/worker.yaml b/omni/machine-classes/worker.yaml index 2626aad..55c6ae2 100644 --- a/omni/machine-classes/worker.yaml +++ b/omni/machine-classes/worker.yaml @@ -5,8 +5,15 @@ metadata: id: oci-worker spec: 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. - # omnictl machine set-labels role=oci-worker - role: oci-worker + # Omni's MachineClass spec takes `matchlabels` (lowercase) as a LIST of + # label selector strings. It was written as a camelCase map, which Omni + # rejects with "machine class should either have auto provision or match + # labels set" — so these classes had never been applied and did not exist + # in Omni at all, which is why oci-lab could not allocate machines. + # + # Set the label on a machine with a MachineLabels resource: + # metadata: {namespace: default, type: MachineLabels.omni.sidero.dev, + # id: , labels: {role: oci-worker}} + # (`omnictl machine set-labels` does not exist in omnictl 1.9.3.) + matchlabels: + - role=oci-worker From 7de5181731899b01380646572332c99e5c36d164 Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Wed, 5 Aug 2026 10:06:08 +0100 Subject: [PATCH 2/8] docs(omni): note that oci-lab patch paths require --allowed-dir --- omni/cluster-templates/oci-lab.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/omni/cluster-templates/oci-lab.yaml b/omni/cluster-templates/oci-lab.yaml index 0943ee2..40c85d4 100644 --- a/omni/cluster-templates/oci-lab.yaml +++ b/omni/cluster-templates/oci-lab.yaml @@ -10,6 +10,10 @@ kubernetes: talos: # Keep in step with installImage in omni/machine-classes/*.yaml. version: v1.13.7 +# Patch paths traverse upward, so every omnictl call against this file needs +# --allowed-dir. `sync` works with it; `status` without it fails with +# failed to access "../patches/cni-none.yaml": path escapes from parent +# Use: omnictl cluster template --allowed-dir "$PWD/omni" -f patches: - file: ../patches/cni-none.yaml - file: ../patches/disable-kube-proxy.yaml From 1a6edbaf34a88d8b85d70e25714b53ae50a4b9ae Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Thu, 6 Aug 2026 09:22:04 +0100 Subject: [PATCH 3/8] feat(oci-lab): add hubble UI tailscale ingress hostname overlay --- values/clusters/oci-lab/cilium.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 values/clusters/oci-lab/cilium.yaml diff --git a/values/clusters/oci-lab/cilium.yaml b/values/clusters/oci-lab/cilium.yaml new file mode 100644 index 0000000..c5116af --- /dev/null +++ b/values/clusters/oci-lab/cilium.yaml @@ -0,0 +1,8 @@ +hubble: + ui: + ingress: + hosts: + - hubble-oci-lab.wind-bearded.ts.net + tls: + - hosts: + - hubble-oci-lab.wind-bearded.ts.net From b383556d2b82819190d8a8d2a0652f9059067e01 Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Fri, 7 Aug 2026 20:24:29 +0100 Subject: [PATCH 4/8] fix(ci): handle multi-doc oci-lab.yaml in version-drift check oci-lab.yaml holds Cluster/ControlPlane/Workers as separate YAML docs; safe_load only grabbed whichever loaded first. Filter to kind: Cluster. --- scripts/check-version-drift.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/check-version-drift.py b/scripts/check-version-drift.py index 7380fb6..72df4ec 100644 --- a/scripts/check-version-drift.py +++ b/scripts/check-version-drift.py @@ -89,7 +89,8 @@ def main(): "Talos pin %s" % (rel, tag, talos) ) else: - doc = yaml.safe_load(text) or {} + docs = [d for d in yaml.safe_load_all(text) if d and d.get("kind") == "Cluster"] + doc = docs[0] if docs else {} got_talos = (doc.get("talos") or {}).get("version") got_k8s = (doc.get("kubernetes") or {}).get("version") if got_talos != talos: From 31806f217869032176ecb371c77c36c008837cf1 Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Fri, 7 Aug 2026 21:33:48 +0100 Subject: [PATCH 5/8] fix(omni): drop upward patch paths from oci-lab template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 8f.6. The oci-lab cluster template referenced its patches as '../patches/...', which escape the template's parent dir. omnictl sync accepted them only with --allowed-dir; status failed outright with 'path escapes from parent'. That forced every CLI invocation to carry --allowed-dir and made the bare form unusable. Move the four oci-lab-only patches into omni/cluster-templates/patches/ (a sibling of the template) and reference them as patches/... — no upward traversal, so neither sync nor status needs --allowed-dir. generate-manifests.sh and docs/deploy/runbook.md pointed at the namespace so they'd write/push to the new path. The omni/patches directory is now empty and removed. Drift gate + selftest still pass; patches are oci-lab-only so nothing else references the former location. --- docs/deploy/runbook.md | 2 +- omni/cluster-templates/oci-lab.yaml | 14 ++++++-------- .../patches/allow-scheduling.yaml | 0 omni/{ => cluster-templates}/patches/cni-none.yaml | 0 .../patches/disable-kube-proxy.yaml | 0 .../patches/oci-lab-inline-manifests.yaml | 0 omni/scripts/generate-manifests.sh | 2 +- 7 files changed, 8 insertions(+), 10 deletions(-) rename omni/{ => cluster-templates}/patches/allow-scheduling.yaml (100%) rename omni/{ => cluster-templates}/patches/cni-none.yaml (100%) rename omni/{ => cluster-templates}/patches/disable-kube-proxy.yaml (100%) rename omni/{ => cluster-templates}/patches/oci-lab-inline-manifests.yaml (100%) diff --git a/docs/deploy/runbook.md b/docs/deploy/runbook.md index 3aea9b5..f62db91 100644 --- a/docs/deploy/runbook.md +++ b/docs/deploy/runbook.md @@ -23,7 +23,7 @@ cluster's identity), and — OCI only — `cilium` block: ```bash mise run oci-lab:generate-manifests mise run unraid:generate-manifests -git add omni/patches/oci-lab-inline-manifests.yaml clusters/unraid-lab/omni/inline-manifests.yaml +git add omni/cluster-templates/patches/oci-lab-inline-manifests.yaml clusters/unraid-lab/omni/inline-manifests.yaml git commit -m "chore: generate inline-manifests for deploy" ``` diff --git a/omni/cluster-templates/oci-lab.yaml b/omni/cluster-templates/oci-lab.yaml index 40c85d4..abea76d 100644 --- a/omni/cluster-templates/oci-lab.yaml +++ b/omni/cluster-templates/oci-lab.yaml @@ -10,15 +10,13 @@ kubernetes: talos: # Keep in step with installImage in omni/machine-classes/*.yaml. version: v1.13.7 -# Patch paths traverse upward, so every omnictl call against this file needs -# --allowed-dir. `sync` works with it; `status` without it fails with -# failed to access "../patches/cni-none.yaml": path escapes from parent -# Use: omnictl cluster template --allowed-dir "$PWD/omni" -f +# Patches live in the sibling patches/ directory, so paths no longer traverse +# upward and no `--allowed-dir` flag is needed. `status` now works without it. patches: - - file: ../patches/cni-none.yaml - - file: ../patches/disable-kube-proxy.yaml - - file: ../patches/allow-scheduling.yaml - - file: ../patches/oci-lab-inline-manifests.yaml + - file: patches/cni-none.yaml + - file: patches/disable-kube-proxy.yaml + - file: patches/allow-scheduling.yaml + - file: patches/oci-lab-inline-manifests.yaml --- # omnictl requires ControlPlane and Workers as SEPARATE documents. They were # nested under the Cluster document, which omnictl rejects outright: diff --git a/omni/patches/allow-scheduling.yaml b/omni/cluster-templates/patches/allow-scheduling.yaml similarity index 100% rename from omni/patches/allow-scheduling.yaml rename to omni/cluster-templates/patches/allow-scheduling.yaml diff --git a/omni/patches/cni-none.yaml b/omni/cluster-templates/patches/cni-none.yaml similarity index 100% rename from omni/patches/cni-none.yaml rename to omni/cluster-templates/patches/cni-none.yaml diff --git a/omni/patches/disable-kube-proxy.yaml b/omni/cluster-templates/patches/disable-kube-proxy.yaml similarity index 100% rename from omni/patches/disable-kube-proxy.yaml rename to omni/cluster-templates/patches/disable-kube-proxy.yaml diff --git a/omni/patches/oci-lab-inline-manifests.yaml b/omni/cluster-templates/patches/oci-lab-inline-manifests.yaml similarity index 100% rename from omni/patches/oci-lab-inline-manifests.yaml rename to omni/cluster-templates/patches/oci-lab-inline-manifests.yaml diff --git a/omni/scripts/generate-manifests.sh b/omni/scripts/generate-manifests.sh index 0c52671..a4e126b 100755 --- a/omni/scripts/generate-manifests.sh +++ b/omni/scripts/generate-manifests.sh @@ -23,7 +23,7 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" case "$CLUSTER" in oci-lab) CLUSTER_TYPE="cloud" - FILE="omni/patches/oci-lab-inline-manifests.yaml" + FILE="omni/cluster-templates/patches/oci-lab-inline-manifests.yaml" WITH_CILIUM=1 ;; unraid-lab) From 3c317c99ff1145fe952258dd8ef30969ce2c7aed Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Fri, 7 Aug 2026 21:24:10 +0100 Subject: [PATCH 6/8] feat(ci): add --write mode to check-version-drift.py Task 4.1b (automate-talos-upgrade-pipeline): make the drift gate capable of repairing the derived files it already validates, not only reporting drift. --write resolves oci-lab's version pairing from the hand-owned omni/versions.yaml and rewrites its owned files (machine classes' installImage tag, cluster template's talos.version/kubernetes.version) to agree with it. Conservative: touches only oci-lab-owned files, never omni/talos-image.yaml (the machine build ledger), and refuses to invent a pairing when oci-lab's kubernetes pin is unresolvable (talos override with no explicit kubernetes). This is the gitops-side half of moving the derived-file rewrite out of the talos-images build job. The build.yml rewiring that invokes it unattended is deliberately deferred (it reverses tested tasks 4.2/4.2a); --write alone is reviewable and independently testable. --- scripts/check-version-drift.py | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/scripts/check-version-drift.py b/scripts/check-version-drift.py index 72df4ec..dba0756 100644 --- a/scripts/check-version-drift.py +++ b/scripts/check-version-drift.py @@ -145,7 +145,91 @@ def selftest(): return 0 +def write(): + """Repair the derived, owned files from versions.yaml, the source of + truth. The counterpart to main(): where main() reports drift, --write + makes the derived files agree with versions.yaml so the gate goes green. + + Conservative by design: + * It only edits files this repo owns and that versions.yaml drives + (oci-lab's machine classes and cluster template). It never touches + omni/talos-image.yaml, a machine-owned build ledger. + * It refuses to write when oci-lab's Kubernetes version cannot be + resolved (a talos override with no explicit kubernetes pin), so it can + never invent a pairing that violates the Kubernetes-never-inherited + rule. + """ + 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") + if k8s is None: + print( + "ERROR: oci-lab overrides talos to %s with no kubernetes pin; " + "cannot write a pairing. Pin kubernetes in versions.yaml first." + % talos + ) + return 1 + + if _write(talos, k8s): + print( + "Rewrote derived files to match omni/versions.yaml " + "(oci-lab: Talos %s, Kubernetes %s)." % (talos, k8s) + ) + return 0 + print("No drift to repair; files already agree with omni/versions.yaml.") + return 0 + + +def _write(talos, k8s): + changed = False + for rel in OCI_LAB_FILES: + full = os.path.join(ROOT, rel) + if not os.path.exists(full): + continue + text = open(full).read() + new = text + + if "machine-classes" in rel: + # Swap the installer image tag (e.g. :v1.13.7) for the pinned Talos + # version, preserving the image path and any variant suffix that + # precedes the tag. + new = re.sub( + r"(\binstallImage:\s*\S+):(v\d[\w.-]*)", + lambda m: "%s:%s" % (m.group(1), talos), + new, + ) + else: + # Rewrite talos.version and kubernetes.version top-level blocks. + # Scope each to its own block so a boundary `version:` line in a + # nested document is never conflated with the one we mean. + new = re.sub( + r"(?m)^kubernetes:\s*\n(\s*version:)\s*\S+", + lambda m: "kubernetes:\n%s %s" % (m.group(1), k8s), + new, + ) + new = re.sub( + r"(?m)^talos:\s*\n(\s*version:)\s*\S+", + lambda m: "talos:\n%s %s" % (m.group(1), talos), + new, + ) + + if new != text: + with open(full, "w") as fh: + fh.write(new) + changed = True + print(" rewrote %s" % rel) + return changed + + if __name__ == "__main__": if "--selftest" in sys.argv: sys.exit(selftest()) + if "--write" in sys.argv: + sys.exit(write()) sys.exit(main()) From 6397112e03a9ed38f895d35f5026bcbca695b60f Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Fri, 7 Aug 2026 21:41:08 +0100 Subject: [PATCH 7/8] feat(ci): guard --write against formerly-unbuilt installer images The previous --write could point oci-lab's machine-class installImage at a ghcr.io/talos-images/installer tag the build had not pushed yet, so a versions.yaml edit ahead of a build would make oci provisioning pull a nonexistent image. installer_built() checks ghcr.io for the tag's manifest (anonymous pull token); write() refuses to proceed when the target installer is missing any --allow-unbuilt, which the build job itself passes once it has pushed the tag. Failures collapse to a clear message rather than silently leaving derived syntax in sync with an image that does not exist. Verified: built pin passes, simulated unbuilt refuses (rc 1), --allow-unbuilt overrides (rc 0). --- scripts/check-version-drift.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/scripts/check-version-drift.py b/scripts/check-version-drift.py index dba0756..7010180 100644 --- a/scripts/check-version-drift.py +++ b/scripts/check-version-drift.py @@ -15,9 +15,13 @@ Runs identically locally and in CI — `python3 scripts/check-version-drift.py`. """ +import json import os import re +import ssl import sys +import urllib.error +import urllib.request import yaml @@ -33,6 +37,53 @@ ] +INSTALLER_IMAGE = "ghcr.io/syscode-labs/talos-images/installer" + + +def installer_built(tag): + """True if the talos-images installer image for this tag already exists in + ghcr.io. + + Machine-class installImage points at this image. The talos-images build + pushes a tag only for versions it has actually built. --write must never + point oci-lab at an installer tag that does not exist, or provisioning + would pull a nonexistent image. The build may legitimately be in flight; + use --allow-unbuilt to override (the talos-images job does, once it has + pushed the tag). + System Python versions (incl. macOS frameworks' 3.7) often lack a working + default SLL trust store; point at the platform CA bundle so the check runs + identically in CI (Ubuntu 3.11) and dev. + """ + repo = INSTALLER_IMAGE[len("ghcr.io/"):] + ctx = ssl.create_default_context() + for cafile in ("/etc/ssl/cert.pem", "/etc/ssl/certs/ca-certificates.crt"): + if os.path.exists(cafile): + ctx = ssl.create_default_context(cafile=cafile) + break + tok = urllib.request.urlopen( + "https://ghcr.io/token?scope=repository:%s:pull" % repo, + context=ctx, + ) + token = json.load(tok)["token"] + req = urllib.request.Request( + "https://ghcr.io/v2/%s/manifests/%s" % (repo, tag), + headers={ + "Authorization": "Bearer %s" % token, + "Accept": "application/vnd.docker.distribution.manifest.list.v2+json, " + "application/vnd.docker.distribution.manifest.v2+json, " + "application/vnd.oci.image.index.v1+json, " + "application/vnd.oci.image.manifest.v1+json", + }, + ) + try: + urllib.request.urlopen(req, context=ctx) + return True + except urllib.error.HTTPError as e: + if e.code == 404: + return False + raise + + def resolve(versions, cluster): """Resolve (talos, kubernetes) for a cluster. @@ -158,6 +209,10 @@ def write(): resolved (a talos override with no explicit kubernetes pin), so it can never invent a pairing that violates the Kubernetes-never-inherited rule. + * It refuses to set machine-class installImage to an installer tag the + talos-images build has not yet pushed (see installer_built), so a + versions.yaml bump to a not-yet-built version cannot point oci-lab at + a nonexistent image. --allow-unbuilt overrides for the build itself. """ path = os.path.join(ROOT, "omni/versions.yaml") with open(path) as fh: @@ -176,6 +231,16 @@ def write(): ) return 1 + allow_unbuilt = "--allow-unbuilt" in sys.argv + if not allow_unbuilt and not installer_built(talos): + print( + "ERROR: installer image %s:%s has not been built yet. The " + "talos-images build must push it before oci-lab can install it. " + "Do not flip versions.yaml ahead of a build. " + "(--allow-unbuilt overrides.)" % (INSTALLER_IMAGE, talos) + ) + return 1 + if _write(talos, k8s): print( "Rewrote derived files to match omni/versions.yaml " From 0ba9f5b11829c285374b8feb5c2b22aa3cc0442e Mon Sep 17 00:00:00 2001 From: Giovanni Ferri Date: Fri, 7 Aug 2026 21:51:20 +0100 Subject: [PATCH 8/8] feat(unraid-lab): add LAN VIP via Cilium native LB-IPAM + L2 announcement MetalLB was considered but this cluster already runs Cilium (kube-proxy replacement mode), which has the same ARP-based L2 VIP mechanism built in. Running MetalLB alongside would race Cilium for L2 announcement of the same addresses. Single /32 (10.10.210.30, verified free against wrt-london's live DHCP+ARP state), opt-in only via the syscode.dev/lan-vip label so it doesn't silently grab the address for an unrelated LoadBalancer Service. Plan: openspec/changes/add-metallb-lan-exposure (syscode-ai-internal-plans). --- .../apps/cilium-lan-vip/application.yaml | 22 +++++++++++++++++++ .../manifests/ipaddresspool.yaml | 12 ++++++++++ .../manifests/kustomization.yaml | 6 +++++ .../manifests/l2announcementpolicy.yaml | 9 ++++++++ values/clusters/unraid-lab/cilium.yaml | 6 +++++ 5 files changed, 55 insertions(+) create mode 100644 clusters/unraid-lab/apps/cilium-lan-vip/application.yaml create mode 100644 clusters/unraid-lab/apps/cilium-lan-vip/manifests/ipaddresspool.yaml create mode 100644 clusters/unraid-lab/apps/cilium-lan-vip/manifests/kustomization.yaml create mode 100644 clusters/unraid-lab/apps/cilium-lan-vip/manifests/l2announcementpolicy.yaml diff --git a/clusters/unraid-lab/apps/cilium-lan-vip/application.yaml b/clusters/unraid-lab/apps/cilium-lan-vip/application.yaml new file mode 100644 index 0000000..b846d0d --- /dev/null +++ b/clusters/unraid-lab/apps/cilium-lan-vip/application.yaml @@ -0,0 +1,22 @@ +# unraid-lab-only. Cilium LB-IPAM + L2 announcement CRs for the LAN VIP +# (openspec: add-metallb-lan-exposure). Cluster-scoped resources — depend +# only on Cilium's own CRDs, which are present before this ever syncs since +# Cilium is the CNI. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cilium-lan-vip + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/syscode-labs/syscode-homelab-gitops-apps.git + targetRevision: HEAD + path: clusters/unraid-lab/apps/cilium-lan-vip/manifests + destination: + server: https://kubernetes.default.svc + namespace: kube-system + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/clusters/unraid-lab/apps/cilium-lan-vip/manifests/ipaddresspool.yaml b/clusters/unraid-lab/apps/cilium-lan-vip/manifests/ipaddresspool.yaml new file mode 100644 index 0000000..7c4d81f --- /dev/null +++ b/clusters/unraid-lab/apps/cilium-lan-vip/manifests/ipaddresspool.yaml @@ -0,0 +1,12 @@ +apiVersion: cilium.io/v2alpha1 +kind: CiliumLoadBalancerIPPool +metadata: + name: unraid-lab-lan +spec: + blocks: + - cidr: 10.10.210.30/32 + # Opt-in only (openspec: add-metallb-lan-exposure, task 1.3). No Service + # gets this VIP unless explicitly labeled — default stays tailnet-only. + serviceSelector: + matchLabels: + syscode.dev/lan-vip: "true" diff --git a/clusters/unraid-lab/apps/cilium-lan-vip/manifests/kustomization.yaml b/clusters/unraid-lab/apps/cilium-lan-vip/manifests/kustomization.yaml new file mode 100644 index 0000000..79146fd --- /dev/null +++ b/clusters/unraid-lab/apps/cilium-lan-vip/manifests/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ipaddresspool.yaml + - l2announcementpolicy.yaml diff --git a/clusters/unraid-lab/apps/cilium-lan-vip/manifests/l2announcementpolicy.yaml b/clusters/unraid-lab/apps/cilium-lan-vip/manifests/l2announcementpolicy.yaml new file mode 100644 index 0000000..90801e3 --- /dev/null +++ b/clusters/unraid-lab/apps/cilium-lan-vip/manifests/l2announcementpolicy.yaml @@ -0,0 +1,9 @@ +apiVersion: cilium.io/v2alpha1 +kind: CiliumL2AnnouncementPolicy +metadata: + name: unraid-lab-lan +spec: + loadBalancerIPs: true + serviceSelector: + matchLabels: + syscode.dev/lan-vip: "true" diff --git a/values/clusters/unraid-lab/cilium.yaml b/values/clusters/unraid-lab/cilium.yaml index f367c6a..972b284 100644 --- a/values/clusters/unraid-lab/cilium.yaml +++ b/values/clusters/unraid-lab/cilium.yaml @@ -1,3 +1,9 @@ +# LAN VIP for opt-in LoadBalancer Services (openspec: add-metallb-lan-exposure). +# Reuses Cilium's native LB-IPAM + L2 announcement instead of a separate +# MetalLB install — same ARP mechanism, already running as the CNI. +l2announcements: + enabled: true + hubble: ui: ingress: