Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d0a18f93 | ||
|
|
4bb040d5a7 | ||
|
|
bdd36d3587 | ||
|
|
dfb8a0834c | ||
|
|
04176ba165 | ||
|
|
dba936b736 | ||
|
|
33c04895f2 | ||
|
|
7ecd10702e | ||
|
|
a6fdcf54f7 | ||
|
|
1a3368e70d | ||
|
|
82165d6704 | ||
|
|
2ff8e216e1 | ||
|
|
dee425fd2f | ||
|
|
59cc813a87 |
@@ -113,8 +113,10 @@ spec:
|
||||
value: "10.244.0.0/16,fd10:244::/48,127.0.0.0/8,::1/128"
|
||||
authentik:
|
||||
log_level: info
|
||||
# Off: reports would go to authentik.error-reporting.a7k.io (Sentry),
|
||||
# which egress policy denies. The outposts take this from the server.
|
||||
error_reporting:
|
||||
enabled: true
|
||||
enabled: false
|
||||
postgresql:
|
||||
host: "ak-pg-rw.authentik.svc.cluster.local"
|
||||
name: "authentik"
|
||||
|
||||
@@ -64,7 +64,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: shelly-manager
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-11-07-1918 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-11-07-2052 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
---
|
||||
# One-shot seed Job. Idempotent — exits 0 immediately if the OTBR pod
|
||||
# already has an active dataset configured. On a clean install (PVC
|
||||
# empty, otbr-agent loaded with --auto-attach=0 so it boots into the
|
||||
# `disabled` state without crashing), it runs the proven bring-up
|
||||
# sequence:
|
||||
# One-shot Job that forms otbr's own Thread network. Idempotent — exits 0
|
||||
# immediately if the OTBR pod already has an active dataset. On a clean
|
||||
# install (PVC empty, otbr-agent loaded with --auto-attach=0 so it boots
|
||||
# into the `disabled` state without crashing), it runs:
|
||||
# 1. ot-ctl ifconfig up (enables PHY on the radio)
|
||||
# 2. ot-ctl dataset set active <hex> (commits dataset; SetMacKey
|
||||
# now succeeds because the radio is enabled)
|
||||
# 3. ot-ctl thread start (MLE attaches; state goes detached → child
|
||||
# → router → leader)
|
||||
# 2. ot-ctl dataset init new, channel, networkname, commit active
|
||||
# (random network key, PAN ID, extended PAN ID and mesh-local
|
||||
# prefix; SetMacKey succeeds because the radio is enabled)
|
||||
# 3. ot-ctl thread start (MLE attaches; state goes detached → leader)
|
||||
#
|
||||
# Channel 15 sits between Wi-Fi channels 1 and 6, clear of the 2.4 GHz
|
||||
# APs (channels 1, 6, 11) and of Zigbee on channel 25. The dataset,
|
||||
# including the network key, lives only on the otbr-thread PVC and in
|
||||
# Home Assistant's thread.datasets (read over the OTBR REST API).
|
||||
#
|
||||
# This sequence is the workaround for SLZB-06P7 firmware behavior where
|
||||
# `dataset set active` triggers an immediate KeyManager push that fails
|
||||
@@ -76,11 +80,10 @@ spec:
|
||||
# (mismatched skew can break exec streaming).
|
||||
image: docker.io/alpine/kubectl:1.37.2@sha256:5e2de75d37e811ba6eb92e06890b1b3263eae4f0a1c17b49269edec589b1352a
|
||||
env:
|
||||
- name: DATASET_TLV
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: otbr-active-dataset
|
||||
key: active-dataset.tlv
|
||||
- name: THREAD_CHANNEL
|
||||
value: "15"
|
||||
- name: THREAD_NETWORK_NAME
|
||||
value: "Suedstr28-Thread"
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
@@ -89,8 +92,8 @@ spec:
|
||||
NS=home-assistant
|
||||
SEL=app.kubernetes.io/name=otbr
|
||||
|
||||
# otbr is disabled (replicas: 0, see deployment.yaml). With
|
||||
# the force annotation, every image bump re-creates this Job;
|
||||
# With otbr scaled to 0 there is no pod to seed. With the
|
||||
# force annotation, every image bump re-creates this Job;
|
||||
# exit 0 instead of waiting for a pod that never comes.
|
||||
replicas=$(kubectl -n "$NS" get deployment otbr \
|
||||
-o jsonpath='{.spec.replicas}')
|
||||
@@ -132,16 +135,19 @@ spec:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "no active dataset; seeding..."
|
||||
echo "no active dataset; forming ${THREAD_NETWORK_NAME} on channel ${THREAD_CHANNEL}..."
|
||||
|
||||
# Order matters: ifconfig up first so the radio's PHY is
|
||||
# ENABLED before dataset commit triggers SetMacKey. Without
|
||||
# this, SetMacKey returns INVALID_STATE and otbr-agent
|
||||
# fatal-exits.
|
||||
kubectl -n "$NS" exec "$pod" -c otbr -- ot-ctl ifconfig up
|
||||
kubectl -n "$NS" exec "$pod" -c otbr -- \
|
||||
ot-ctl dataset set active "$DATASET_TLV"
|
||||
kubectl -n "$NS" exec "$pod" -c otbr -- ot-ctl thread start
|
||||
ot() { kubectl -n "$NS" exec "$pod" -c otbr -- ot-ctl "$@"; }
|
||||
ot ifconfig up
|
||||
ot dataset init new
|
||||
ot dataset channel "$THREAD_CHANNEL"
|
||||
ot dataset networkname "$THREAD_NETWORK_NAME"
|
||||
ot dataset commit active
|
||||
ot thread start
|
||||
|
||||
# Verify Thread came up.
|
||||
for i in $(seq 1 30); do
|
||||
|
||||
@@ -5,16 +5,11 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: otbr
|
||||
spec:
|
||||
# DISABLED (replicas: 0). Running otbr broke Matter: its radio is RF-isolated
|
||||
# enough from the Nest mesh that it forms a rival partition of the same
|
||||
# network, and the two bridge only over Ethernet — fast enough for a ping,
|
||||
# too slow for the fragmented CASE handshake, so devices stay discoverable
|
||||
# while every commissioning attempt times out.
|
||||
#
|
||||
# The instability is at the Thread partition layer, not in the config below.
|
||||
# Re-enable only once co-BR integration is verified to MERGE: one partition,
|
||||
# one OMR prefix, and the Nest routers visible in otbr's router table.
|
||||
replicas: 0
|
||||
# otbr runs its own Thread network, formed by otbr-dataset-bootstrap. It is
|
||||
# not a co-BR of the Nest's NEST-PAN-339C: its radio does not hear that mesh
|
||||
# well enough to merge partitions, and partitions bridged only over
|
||||
# Ethernet are too slow for Matter's CASE handshake.
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
@@ -34,49 +29,48 @@ spec:
|
||||
# RCP is reached over the network (slzb-thread.renner.uno).
|
||||
nodeSelector:
|
||||
net.renner.uno/iot-vlan: "true"
|
||||
initContainers:
|
||||
# The Thread RCP is a network-attached radio (spinel over TCP).
|
||||
# otbr-agent speaks spinel+hdlc over a Unix stream socket, which this
|
||||
# native sidecar serves on the shared emptyDir and bridges to the
|
||||
# radio, one TCP connection per agent connection. A PTY would not
|
||||
# work here: devpts is per-container.
|
||||
- name: radio-bridge
|
||||
image: alpine/socat:1.8.1.3@sha256:466c8a0383e16463fabdb7df01008292fb78d07da60224a46cb5d3142ce122ed
|
||||
imagePullPolicy: IfNotPresent
|
||||
restartPolicy: Always
|
||||
args:
|
||||
- -d
|
||||
- UNIX-LISTEN:/run/thread/spinel.sock,fork,unlink-early,mode=600
|
||||
- TCP:slzb-thread.renner.uno:6638
|
||||
startupProbe:
|
||||
exec:
|
||||
command: ["test", "-S", "/run/thread/spinel.sock"]
|
||||
periodSeconds: 1
|
||||
failureThreshold: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: radio
|
||||
mountPath: /run/thread
|
||||
containers:
|
||||
# The Thread RCP is a network-attached radio (Serial-over-IP), so the
|
||||
# spinel transport needs a TCP-to-PTY bridge. Two obvious approaches
|
||||
# do not work and should not be retried:
|
||||
#
|
||||
# - `spinel+hdlc+forkpty://` needs socat, which the upstream image
|
||||
# does not ship; the URL fails with a silent ENOENT and otbr-agent
|
||||
# dies right after logging it.
|
||||
# - A sidecar cannot provide it either: devpts is per-container, so
|
||||
# a PTY created next door is invisible here and the symlink dangles.
|
||||
#
|
||||
# Hence socat is installed at startup and run inside this container.
|
||||
- name: otbr
|
||||
image: openthread/otbr:latest@sha256:51f9af250252f4faae85d0197574a7c17ada873c78799854461a93e3e42cf324
|
||||
image: openthread/otbr:latest@sha256:cada244b39d1f43cb12d5f93694b4621db3b51c28219a05bbffac0f898d1403e
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
set -euo pipefail
|
||||
if ! command -v socat >/dev/null; then
|
||||
echo "otbr: installing socat from apt"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq socat
|
||||
fi
|
||||
echo "otbr: starting TCP↔PTY bridge to ${SLZB_HOST}:${SLZB_PORT}"
|
||||
# Match the HA addon's socat invocation byte-for-byte
|
||||
# (rootfs/etc/s6-overlay/s6-rc.d/socat-otbr-tcp/run): the
|
||||
# known-working reference for SMLight network-attached
|
||||
# Thread radios. ignoreeof keeps the data loop alive when
|
||||
# otbr-agent transiently closes the PTY (e.g. on agent
|
||||
# restart from livenessProbe), instead of socat exiting
|
||||
# and racing to recreate the PTY symlink.
|
||||
( while true; do
|
||||
socat -d \
|
||||
"pty,raw,echo=0,link=/tmp/ttyThread,ignoreeof" \
|
||||
"tcp:${SLZB_HOST}:${SLZB_PORT}"
|
||||
echo "otbr: socat bridge exited; restarting in 2s" >&2
|
||||
sleep 2
|
||||
done ) &
|
||||
until [ -e /tmp/ttyThread ]; do
|
||||
echo "otbr: waiting for serial bridge PTY at /tmp/ttyThread"
|
||||
sleep 1
|
||||
done
|
||||
# SLZB-06P7 firmware crashes otbr-agent if a saved dataset
|
||||
# is loaded before the radio's PHY is enabled: dataset
|
||||
# commit triggers KeyManager → otPlatRadioSetMacKey, which
|
||||
@@ -99,8 +93,8 @@ spec:
|
||||
/app/etc/docker/test/docker_entrypoint.sh &
|
||||
ENTRYPOINT_PID=$!
|
||||
# Wait for the daemon socket to come up. Up to 90s of
|
||||
# attempts at 2s intervals — covers slow apt installs,
|
||||
# spinel handshake, and agent init on this radio.
|
||||
# attempts at 2s intervals — covers the spinel handshake
|
||||
# and agent init on this radio.
|
||||
for i in $(seq 1 45); do
|
||||
if ot-ctl state >/dev/null 2>&1; then
|
||||
break
|
||||
@@ -172,16 +166,9 @@ spec:
|
||||
fi
|
||||
wait "$ENTRYPOINT_PID"
|
||||
env:
|
||||
- name: SLZB_HOST
|
||||
value: "slzb-thread.renner.uno"
|
||||
- name: SLZB_PORT
|
||||
value: "6638"
|
||||
# Match the HA addon's RADIO_URL byte-for-byte. baud is
|
||||
# omitted because PTYs ignore termios speed; uart-flow-
|
||||
# control is a no-op on a PTY but stays in to mirror the
|
||||
# known-working reference exactly.
|
||||
# The radio-bridge sidecar's socket.
|
||||
- name: RADIO_URL
|
||||
value: "spinel+hdlc+uart:///tmp/ttyThread?uart-flow-control"
|
||||
value: "spinel+hdlc+unix:///run/thread/spinel.sock"
|
||||
# Use the macvlan attachment as the backbone interface. mDNS
|
||||
# advertisements, SRP server listening, and RA reception all
|
||||
# happen on this NIC. eth0 (the Cilium pod CIDR) is the
|
||||
@@ -254,6 +241,8 @@ spec:
|
||||
# and re-election on every restart).
|
||||
- name: data
|
||||
mountPath: /var/lib/thread
|
||||
- name: radio
|
||||
mountPath: /run/thread
|
||||
# Probes must watch the *protocol*, not just the process.
|
||||
#
|
||||
# The upstream entrypoint backgrounds otbr-agent and ends in
|
||||
@@ -271,8 +260,7 @@ spec:
|
||||
# from this image, so probes go through ot-ctl, not REST.
|
||||
#
|
||||
# startupProbe: the agent's control socket answers. Covers the
|
||||
# apt install, the socat TCP↔PTY bridge to the SLZB radio, and
|
||||
# the spinel handshake. Generous: 10s × 60 = 10 minutes. While
|
||||
# spinel handshake with the SLZB radio and agent init. Generous: 10s × 60 = 10 minutes. While
|
||||
# it runs, the other two probes are suspended, so the entrypoint
|
||||
# gets to finish activating Thread before liveness may act.
|
||||
startupProbe:
|
||||
@@ -344,4 +332,6 @@ spec:
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: otbr-data
|
||||
claimName: otbr-thread
|
||||
- name: radio
|
||||
emptyDir: {}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: otbr-active-dataset
|
||||
namespace: home-assistant
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: openbao
|
||||
target:
|
||||
name: otbr-active-dataset # keeps the old Secret's name — consumers unchanged
|
||||
dataFrom:
|
||||
- extract:
|
||||
key: apps/otbr
|
||||
@@ -3,8 +3,12 @@ kind: Kustomization
|
||||
namespace: home-assistant
|
||||
resources:
|
||||
- dataset-bootstrap.yaml
|
||||
- externalsecret.yaml
|
||||
- deployment.yaml
|
||||
- network-attachment.yaml
|
||||
- pvc.yaml
|
||||
- service.yaml
|
||||
- thread-migrate.yaml
|
||||
configMapGenerator:
|
||||
- name: thread-migrate
|
||||
files:
|
||||
- thread-migrate.py
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: otbr-data
|
||||
name: otbr-thread
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Move Matter-over-Thread nodes onto otbr's Thread network.
|
||||
|
||||
Per node, over the existing CASE session (no re-commissioning):
|
||||
ArmFailSafe -> RemoveNetwork(old) -> AddOrUpdateThreadNetwork(otbr dataset)
|
||||
-> ConnectNetwork(new) -> CommissioningComplete over the new network.
|
||||
If CommissioningComplete never arrives, the fail-safe expires and the node
|
||||
reverts to its previous network configuration on its own.
|
||||
|
||||
The dataset (incl. network key) is read from otbr's REST API and only ever
|
||||
passed to matter-server; it is never logged.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import aiohttp
|
||||
from chip.clusters import Objects as C
|
||||
from matter_server.client import MatterClient
|
||||
|
||||
OTBR_URL = os.environ["OTBR_URL"]
|
||||
MATTER_SERVER_URL = os.environ["MATTER_SERVER_URL"]
|
||||
NODE_IDS = [int(n) for n in os.environ["NODE_IDS"].split()]
|
||||
FAILSAFE_SECONDS = int(os.environ.get("FAILSAFE_SECONDS", "180"))
|
||||
|
||||
EXT_PAN_ATTR = "0/53/4" # ThreadNetworkDiagnostics.ExtendedPanId
|
||||
MAX_NETWORKS_ATTR = "0/49/0" # NetworkCommissioning.MaxNetworks
|
||||
|
||||
log = logging.getLogger("thread-migrate")
|
||||
|
||||
|
||||
def extended_pan_id(tlv: bytes) -> bytes:
|
||||
"""Return the Extended PAN ID (TLV type 2) of an operational dataset."""
|
||||
i = 0
|
||||
while i + 2 <= len(tlv):
|
||||
tlv_type, length = tlv[i], tlv[i + 1]
|
||||
if tlv_type == 2:
|
||||
return tlv[i + 2 : i + 2 + length]
|
||||
i += 2 + length
|
||||
raise ValueError("dataset has no Extended PAN ID")
|
||||
|
||||
|
||||
async def read_dataset(session: aiohttp.ClientSession) -> bytes:
|
||||
async with session.get(
|
||||
f"{OTBR_URL}/node/dataset/active", headers={"Accept": "text/plain"}
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
return bytes.fromhex((await resp.text()).strip())
|
||||
|
||||
|
||||
async def read_value(client: MatterClient, node_id: int, path: str):
|
||||
values = await client.read_attribute(node_id, path)
|
||||
return values[path]
|
||||
|
||||
|
||||
async def command(client: MatterClient, node_id: int, cmd, timeout: float = 60):
|
||||
return await asyncio.wait_for(
|
||||
client.send_device_command(node_id, 0, cmd), timeout=timeout
|
||||
)
|
||||
|
||||
|
||||
async def migrate(client: MatterClient, node_id: int, tlv: bytes, new_ext: bytes) -> bool:
|
||||
current = await read_value(client, node_id, EXT_PAN_ATTR)
|
||||
log.info("node %s: extended PAN %016x", node_id, current)
|
||||
if current == int.from_bytes(new_ext, "big"):
|
||||
log.info("node %s: already on otbr's network", node_id)
|
||||
return True
|
||||
old_ext = current.to_bytes(8, "big")
|
||||
max_networks = await read_value(client, node_id, MAX_NETWORKS_ATTR)
|
||||
|
||||
log.info("node %s: ArmFailSafe %ss", node_id, FAILSAFE_SECONDS)
|
||||
resp = await command(
|
||||
client,
|
||||
node_id,
|
||||
C.GeneralCommissioning.Commands.ArmFailSafe(
|
||||
expiryLengthSeconds=FAILSAFE_SECONDS, breadcrumb=1
|
||||
),
|
||||
)
|
||||
log.info("node %s: ArmFailSafe -> %s", node_id, resp)
|
||||
|
||||
if max_networks <= 1:
|
||||
resp = await command(
|
||||
client,
|
||||
node_id,
|
||||
C.NetworkCommissioning.Commands.RemoveNetwork(networkID=old_ext, breadcrumb=2),
|
||||
)
|
||||
log.info("node %s: RemoveNetwork(old) -> %s", node_id, resp)
|
||||
|
||||
resp = await command(
|
||||
client,
|
||||
node_id,
|
||||
C.NetworkCommissioning.Commands.AddOrUpdateThreadNetwork(
|
||||
operationalDataset=tlv, breadcrumb=3
|
||||
),
|
||||
)
|
||||
log.info("node %s: AddOrUpdateThreadNetwork -> %s", node_id, resp)
|
||||
|
||||
# The node leaves the old network while answering; a missing response
|
||||
# is expected.
|
||||
try:
|
||||
resp = await command(
|
||||
client,
|
||||
node_id,
|
||||
C.NetworkCommissioning.Commands.ConnectNetwork(networkID=new_ext, breadcrumb=4),
|
||||
timeout=90,
|
||||
)
|
||||
log.info("node %s: ConnectNetwork -> %s", node_id, resp)
|
||||
except Exception as err: # noqa: BLE001
|
||||
log.info("node %s: ConnectNetwork without response (%r)", node_id, err)
|
||||
|
||||
deadline = asyncio.get_running_loop().time() + FAILSAFE_SECONDS - 20
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
try:
|
||||
resp = await command(
|
||||
client,
|
||||
node_id,
|
||||
C.GeneralCommissioning.Commands.CommissioningComplete(),
|
||||
timeout=30,
|
||||
)
|
||||
log.info("node %s: CommissioningComplete -> %s", node_id, resp)
|
||||
break
|
||||
except Exception as err: # noqa: BLE001
|
||||
log.info("node %s: CommissioningComplete not yet (%r)", node_id, err)
|
||||
await asyncio.sleep(10)
|
||||
else:
|
||||
log.error("node %s: no CommissioningComplete; fail-safe reverts it", node_id)
|
||||
return False
|
||||
|
||||
current = await read_value(client, node_id, EXT_PAN_ATTR)
|
||||
ok = current == int.from_bytes(new_ext, "big")
|
||||
log.info("node %s: extended PAN now %016x (%s)", node_id, current, "ok" if ok else "MISMATCH")
|
||||
return ok
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tlv = await read_dataset(session)
|
||||
new_ext = extended_pan_id(tlv)
|
||||
log.info("otbr network extended PAN %s", new_ext.hex())
|
||||
|
||||
client = MatterClient(MATTER_SERVER_URL, session)
|
||||
ready = asyncio.Event()
|
||||
listener = asyncio.create_task(client.start_listening(ready))
|
||||
await ready.wait()
|
||||
|
||||
failed = []
|
||||
for node_id in NODE_IDS:
|
||||
try:
|
||||
if not await migrate(client, node_id, tlv, new_ext):
|
||||
failed.append(node_id)
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("node %s: migration failed", node_id)
|
||||
failed.append(node_id)
|
||||
|
||||
listener.cancel()
|
||||
await client.disconnect()
|
||||
if failed:
|
||||
log.error("failed nodes: %s", failed)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
|
||||
sys.exit(asyncio.run(main()))
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
# Moves the Matter-over-Thread nodes in NODE_IDS from the Nest's Thread
|
||||
# network onto otbr's, without re-commissioning (see thread-migrate.py).
|
||||
# Idempotent: nodes already on otbr's network are skipped. NODE_IDS are
|
||||
# matter-server node IDs.
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: thread-migrate
|
||||
annotations:
|
||||
# Changing NODE_IDS changes the pod template; recreate the Job for it.
|
||||
kustomize.toolkit.fluxcd.io/force: enabled
|
||||
spec:
|
||||
# A failed node reverts by itself via its fail-safe; no blind retries.
|
||||
backoffLimit: 0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: thread-migrate
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: thread-migrate
|
||||
# Same image as matter-server: ships matter_server.client and the
|
||||
# chip cluster definitions matching the server.
|
||||
image: ghcr.io/matter-js/python-matter-server:8.1.2
|
||||
command: ["python3", "-I", "/scripts/thread-migrate.py"]
|
||||
env:
|
||||
# Empty: nothing to migrate.
|
||||
- name: NODE_IDS
|
||||
value: ""
|
||||
- name: OTBR_URL
|
||||
value: "http://otbr.home-assistant.svc.cluster.local:8081"
|
||||
- name: MATTER_SERVER_URL
|
||||
value: "ws://matter-server.home-assistant.svc.cluster.local:5580/ws"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
volumeMounts:
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: scripts
|
||||
configMap:
|
||||
name: thread-migrate
|
||||
@@ -91,8 +91,6 @@ spec:
|
||||
name: flux-system
|
||||
path: ./kubernetes/apps/otbr
|
||||
prune: true
|
||||
# Not in use.
|
||||
suspend: true
|
||||
wait: true
|
||||
# decryption block dropped 2026-07-13: path is sops-free — secrets come
|
||||
# from OpenBao via ESO (MIGRATION-sops-openbao.md).
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageRepository
|
||||
# 6h, not 1h: both repositories carry tens to hundreds of thousands of PR
|
||||
# build tags, and every scan pages the full list anonymously, which runs
|
||||
# into ghcr.io's rate limit.
|
||||
metadata:
|
||||
name: immich-server
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: ghcr.io/immich-app/immich-server
|
||||
interval: 1h
|
||||
interval: 6h
|
||||
---
|
||||
# v-prefix tags only, semver 3.x. The 4.x cutover would need manual
|
||||
# review — Immich major bumps historically require migration.
|
||||
@@ -31,7 +34,7 @@ metadata:
|
||||
namespace: flux-system
|
||||
spec:
|
||||
image: ghcr.io/immich-app/immich-machine-learning
|
||||
interval: 1h
|
||||
interval: 6h
|
||||
---
|
||||
# Same policy as immich-server; machine-learning image must stay in
|
||||
# version-sync with the server image (Immich docs require it).
|
||||
|
||||
@@ -467,7 +467,8 @@ spec:
|
||||
- port: "443"
|
||||
protocol: TCP
|
||||
---
|
||||
# zigbee2mqtt: Zigbee coordinator (SLZB) TCP socket.
|
||||
# zigbee2mqtt: Zigbee coordinator (SLZB) TCP socket, and the OTA index and
|
||||
# firmware images (Koenkk/zigbee-OTA) on raw.githubusercontent.com.
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
@@ -498,6 +499,79 @@ spec:
|
||||
- ports:
|
||||
- port: "6638"
|
||||
protocol: TCP
|
||||
- toFQDNs:
|
||||
- matchName: "raw.githubusercontent.com"
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "443"
|
||||
protocol: TCP
|
||||
---
|
||||
# matter-server: the CSA DCL (PAA root certificates, vendor names, OTA
|
||||
# metadata) and the development PAA certificates of project-chip on GitHub.
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: matter-server-egress
|
||||
namespace: home-assistant
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matter-server
|
||||
egress:
|
||||
# rules.dns lets Cilium learn the toFQDNs mapping
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
"k8s:io.kubernetes.pod.namespace": kube-system
|
||||
k8s-app: kube-dns
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "53"
|
||||
protocol: UDP
|
||||
- port: "53"
|
||||
protocol: TCP
|
||||
rules:
|
||||
dns:
|
||||
- matchPattern: "*"
|
||||
- toFQDNs:
|
||||
- matchName: "on.dcl.csa-iot.org"
|
||||
- matchName: "api.github.com"
|
||||
- matchName: "raw.githubusercontent.com"
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "443"
|
||||
protocol: TCP
|
||||
---
|
||||
# otbr: the radio-bridge sidecar's spinel link to the SLZB-06P7 Thread radio.
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: otbr-egress
|
||||
namespace: home-assistant
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: otbr
|
||||
egress:
|
||||
# rules.dns lets Cilium learn the toFQDNs mapping
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
"k8s:io.kubernetes.pod.namespace": kube-system
|
||||
k8s-app: kube-dns
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "53"
|
||||
protocol: UDP
|
||||
- port: "53"
|
||||
protocol: TCP
|
||||
rules:
|
||||
dns:
|
||||
- matchPattern: "*"
|
||||
- toFQDNs:
|
||||
- matchName: "slzb-thread.renner.uno"
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "6638"
|
||||
protocol: TCP
|
||||
---
|
||||
# mqtt-account-controller: credential delivery to deliver.esp32 targets in mqtt-accounts.yaml.
|
||||
apiVersion: cilium.io/v2
|
||||
|
||||
Reference in New Issue
Block a user