From 8407feaa75bed9ca5962b54de66bdb286f2799b8 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 4 Jun 2026 19:31:54 +0200 Subject: [PATCH 1/4] feat: validate -config against Talos network configuration schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the generic YAML well-formedness check with a strict decode into network.PlatformConfigSpec from the Talos machinery module — the same type Talos uses to parse META key 0xa. Unknown fields, malformed addresses, invalid enum values and extra YAML documents are now rejected before anything is written to the META partition. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- README.md | 2 + go.mod | 44 +++++++++++++- go.sum | 145 ++++++++++++++++++++++++++++++++++++++++++++--- main.go | 16 +----- main_test.go | 23 -------- validate.go | 36 ++++++++++++ validate_test.go | 104 +++++++++++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 47 deletions(-) create mode 100644 validate.go create mode 100644 validate_test.go diff --git a/README.md b/README.md index 47b958b..4add72d 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,5 @@ talos-meta-tool -device /dev/sda -config config.yaml ``` The `-device` flag accepts the full disk (e.g. `/dev/sda`); the META partition is discovered automatically via GPT. + +The `-config` file is validated against the Talos metal network configuration schema (`network.PlatformConfigSpec`) before writing: unknown fields, malformed addresses and invalid enum values are rejected. diff --git a/go.mod b/go.mod index 53af0d1..0b56d23 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,49 @@ require ( github.com/google/uuid v1.6.0 github.com/siderolabs/go-adv v1.0.0 github.com/siderolabs/go-blockdevice/v2 v2.0.26 - gopkg.in/yaml.v3 v3.0.1 + github.com/siderolabs/talos/pkg/machinery v1.13.3 + go.yaml.in/yaml/v4 v4.0.0-rc.4 ) require ( + cel.dev/expr v0.25.1 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/containerd/go-cni v1.1.13 // indirect + github.com/containernetworking/cni v1.3.0 // indirect + github.com/cosi-project/runtime v1.14.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gertd/go-pluralize v0.2.1 // indirect + github.com/google/cel-go v0.27.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260317095713-310581b9c6ac // indirect + github.com/mdlayher/ethtool v0.5.1 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.9.0 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/opencontainers/runtime-spec v1.3.0 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/siderolabs/crypto v0.6.5 // indirect github.com/siderolabs/gen v0.8.6 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect + github.com/siderolabs/go-pointer v1.0.1 // indirect + github.com/siderolabs/net v0.4.0 // indirect + github.com/siderolabs/protoenc v0.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect ) diff --git a/go.sum b/go.sum index e73a2c5..2ce5272 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,153 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/brianvoe/gofakeit/v7 v7.7.3 h1:RWOATEGpJ5EVg2nN8nlaEyaV/aB4d6c3GqYrbqQekss= +github.com/brianvoe/gofakeit/v7 v7.7.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= +github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= +github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7Hnis= +github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI= +github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= +github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= +github.com/cosi-project/runtime v1.14.1 h1:1mxuH0zGXdJIy6762kaQsd+7C9MmzzuvIVIfWd867Os= +github.com/cosi-project/runtime v1.14.1/go.mod h1:SfzpfNx7YwK8byi1X6ytikDXVMmbC7UpiCWdzntRf8M= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/freddierice/go-losetup/v2 v2.0.1 h1:wPDx/Elu9nDV8y/CvIbEDz5Xi5Zo80y4h7MKbi3XaAI= github.com/freddierice/go-losetup/v2 v2.0.1/go.mod h1:TEyBrvlOelsPEhfWD5rutNXDmUszBXuFnwT1kIQF4J8= +github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260317095713-310581b9c6ac h1:UfziP9RaDM6D+f+yNdL3T/N1DztwltLDGBkpybF/fYs= +github.com/jsimonetti/rtnetlink/v2 v2.2.1-0.20260317095713-310581b9c6ac/go.mod h1:A/gqt1BEMJcvzGQJXQ3SnsDOQL7QRNhxTiC3eb++608= +github.com/mdlayher/ethtool v0.5.1 h1:U4GThY6WgNJUJsMrUzBmoOTdQHFWxFPTHTeNnn3GCvU= +github.com/mdlayher/ethtool v0.5.1/go.mod h1:Pz39PaAy96Ea1SrCvEO/pPEAeULvRJjO6zspuEMhJy4= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= +github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg= +github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/siderolabs/crypto v0.6.5 h1:Elq5tpWP2ApZ4Y+Kg+eIDiiWbmriCPI1mjYIMwvsYkw= +github.com/siderolabs/crypto v0.6.5/go.mod h1:QjVcrdJQE1sxhjHqieCgwGdlIYq/xCP2DL53Up8nbU4= github.com/siderolabs/gen v0.8.6 h1:pE6shuqov3L+5rEcAUJ/kY6iJofimljQw5G95P8a5c4= github.com/siderolabs/gen v0.8.6/go.mod h1:J9IbusbES2W6QWjtSHpDV9iPGZHc978h1+KJ4oQRspQ= github.com/siderolabs/go-adv v1.0.0 h1:ZWXnoGq1GKeEIkLSR4o6oKcayFsowZkJsWcyQqwPk6c= github.com/siderolabs/go-adv v1.0.0/go.mod h1:nR6YwduJv56mZI1D3ow1Zok5lwefiM94hS0o1d6KmNc= github.com/siderolabs/go-blockdevice/v2 v2.0.26 h1:t7faVJft7OrC/INPpODKg79O4qVpeKlkbs3amk/DIdQ= github.com/siderolabs/go-blockdevice/v2 v2.0.26/go.mod h1:a6KUjzyU8Joo7y9cW9BdmORCFJwVNweHYRpKiuDfMU8= +github.com/siderolabs/go-pointer v1.0.1 h1:f7Yi4IK1jptS8yrT9GEbwhmGcVxvPQgBUG/weH3V3DM= +github.com/siderolabs/go-pointer v1.0.1/go.mod h1:C8Q/3pNHT4RE9e4rYR9PHeS6KPMlStRBgYrJQJNy/vA= +github.com/siderolabs/go-retry v0.3.3 h1:zKV+S1vumtO72E6sYsLlmIdV/G/GcYSBLiEx/c9oCEg= +github.com/siderolabs/go-retry v0.3.3/go.mod h1:Ff/VGc7v7un4uQg3DybgrmOWHEmJ8BzZds/XNn/BqMI= +github.com/siderolabs/net v0.4.0 h1:1bOgVay/ijPkJz4qct98nHsiB/ysLQU0KLoBC4qLm7I= +github.com/siderolabs/net v0.4.0/go.mod h1:/ibG+Hm9HU27agp5r9Q3eZicEfjquzNzQNux5uEk0kM= +github.com/siderolabs/protoenc v0.2.4 h1:D3Fpn2nQSQOhl8ZlAxijZAf7K6F8CM1uZq0afIGsr8Q= +github.com/siderolabs/protoenc v0.2.4/go.mod h1:i5XLHjfv5vyi7LhQrSEo19HCA+lYtDd7CWxsoWp9XE8= +github.com/siderolabs/talos/pkg/machinery v1.13.3 h1:+8pPT7UZUSxbmwAor81/uleQla9cItwww1Aps4XuQPI= +github.com/siderolabs/talos/pkg/machinery v1.13.3/go.mod h1:RdU2CNuyxPLXpxVYgJlK3XxIw0QIleSR3I8T+x1cxZU= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA= +google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 622a35e..5e57ce1 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,6 @@ import ( "github.com/siderolabs/go-adv/adv/talos" "github.com/siderolabs/go-blockdevice/v2/block" "github.com/siderolabs/go-blockdevice/v2/partitioning/gpt" - "gopkg.in/yaml.v3" ) const FixedTag = 0xA // Fixed tag @@ -65,14 +64,6 @@ func findMetaPartition(f *os.File) (interface{ io.ReaderAt; io.WriterAt }, error return nil, fmt.Errorf("META partition not found") } -func validateYAML(data []byte) ([]byte, error) { - var config interface{} - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, err - } - return yaml.Marshal(config) -} - func writeConfig(dev interface{ io.ReaderAt; io.WriterAt }, configData []byte) error { adv, loadErr := talos.NewADV(io.NewSectionReader(dev, 0, int64(talos.Size))) if adv == nil { @@ -111,9 +102,8 @@ func main() { log.Fatalf("Error reading configuration file: %v", err) } - validatedConfigData, err := validateYAML(configData) - if err != nil { - log.Fatalf("Invalid YAML configuration: %v", err) + if err := validateConfig(configData); err != nil { + log.Fatalf("Invalid network configuration: %v", err) } device, err := os.OpenFile(*devicePath, os.O_RDWR, 0) @@ -127,7 +117,7 @@ func main() { log.Fatalf("Error: %v", err) } - if err := writeConfig(meta, validatedConfigData); err != nil { + if err := writeConfig(meta, configData); err != nil { log.Fatalf("Error: %v", err) } diff --git a/main_test.go b/main_test.go index 8191210..234f5b4 100644 --- a/main_test.go +++ b/main_test.go @@ -113,29 +113,6 @@ func (errDevice) WriteAt(p []byte, off int64) (int, error) { return 0, errors.New("device error") } -func TestValidateYAMLValid(t *testing.T) { - out, err := validateYAML([]byte("key: value\nfoo: bar\n")) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - // Output must itself be valid and stable (idempotent). - out2, err := validateYAML(out) - if err != nil { - t.Fatalf("output is not valid YAML: %v", err) - } - - if !bytes.Equal(out, out2) { - t.Fatalf("validateYAML not idempotent:\n first: %q\nsecond: %q", out, out2) - } -} - -func TestValidateYAMLInvalid(t *testing.T) { - if _, err := validateYAML([]byte("key: [\ninvalid")); err == nil { - t.Fatal("expected error for invalid YAML, got nil") - } -} - func TestWriteConfigRoundTrip(t *testing.T) { f := newTestFile(t) diff --git a/validate.go b/validate.go new file mode 100644 index 0000000..6e6b769 --- /dev/null +++ b/validate.go @@ -0,0 +1,36 @@ +package main + +import ( + "bytes" + "errors" + "io" + + "github.com/siderolabs/talos/pkg/machinery/resources/network" + "go.yaml.in/yaml/v4" +) + +// validateConfig strictly decodes data as the metal platform network +// configuration (the format Talos reads from META key 0x0a), rejecting +// unknown fields, malformed values and extra YAML documents. +// +// See https://docs.siderolabs.com/talos/latest/networking/metal-network-configuration. +func validateConfig(data []byte) error { + dec := yaml.NewDecoder(bytes.NewReader(data)) + dec.KnownFields(true) + + var cfg network.PlatformConfigSpec + + if err := dec.Decode(&cfg); err != nil { + if errors.Is(err, io.EOF) { + return errors.New("configuration is empty") + } + + return err + } + + if err := dec.Decode(new(any)); !errors.Is(err, io.EOF) { + return errors.New("unexpected extra YAML document") + } + + return nil +} diff --git a/validate_test.go b/validate_test.go new file mode 100644 index 0000000..7e98915 --- /dev/null +++ b/validate_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "strings" + "testing" +) + +// validNetworkConfig follows the metal network configuration format, +// see https://docs.siderolabs.com/talos/latest/networking/metal-network-configuration. +const validNetworkConfig = `addresses: + - address: 147.75.61.43/31 + linkName: bond0 + family: inet4 + scope: global + flags: permanent + layer: platform +links: + - name: eth0 + up: true + layer: platform + - name: bond0 + logical: true + up: true + mtu: 1500 + kind: bond + type: ether + bondMaster: + mode: 802.3ad + xmitHashPolicy: layer3+4 + lacpRate: fast + miimon: 100 + updelay: 300 + downdelay: 200 + layer: platform +routes: + - family: inet4 + gateway: 147.75.61.42 + outLinkName: bond0 + table: main + scope: global + type: unicast + protocol: static + layer: platform +hostnames: + - hostname: ci-blue-worker-amd64-2 + layer: platform +resolvers: + - dnsServers: + - 8.8.8.8 + - 1.1.1.1 + layer: platform +timeServers: + - timeServers: + - pool.ntp.org + layer: platform +operators: + - operator: dhcp4 + linkName: eth1 + requireUp: true + dhcp4: + routeMetric: 1024 + layer: platform +externalIPs: + - 147.75.61.43 +` + +func TestValidateConfigValid(t *testing.T) { + if err := validateConfig([]byte(validNetworkConfig)); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestValidateConfigInvalid(t *testing.T) { + for _, tt := range []struct { + name string + config string + }{ + {"malformed YAML", "key: [\ninvalid"}, + {"empty", ""}, + {"unknown top-level field", "hostname: talos-test\n"}, + {"unknown nested field", "addresses:\n - adress: 1.2.3.4/32\n"}, + {"wrong type", "addresses: notalist\n"}, + {"bad enum value", "addresses:\n - address: 1.2.3.4/32\n family: inet5\n"}, + {"bad IP", "externalIPs:\n - not-an-ip\n"}, + {"extra document", validNetworkConfig + "---\nexternalIPs: []\n"}, + } { + t.Run(tt.name, func(t *testing.T) { + if err := validateConfig([]byte(tt.config)); err == nil { + t.Fatalf("expected error, got nil") + } + }) + } +} + +func TestValidateConfigUnknownFieldError(t *testing.T) { + err := validateConfig([]byte("addresses: []\nfoo: bar\n")) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "foo") { + t.Fatalf("error should mention the unknown field, got: %v", err) + } +} From 5c202de47cf22313bb94f68f0e433051d0377fb2 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 4 Jun 2026 19:48:04 +0200 Subject: [PATCH 2/4] docs: point to Talos v1.13 metal network configuration docs Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- README.md | 4 ++-- validate.go | 2 +- validate_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4add72d..8a55eba 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Tool for writing network metadata in the Talos META partition. -Doc: https://www.talos.dev/v1.8/advanced/metal-network-configuration/ +Doc: https://docs.siderolabs.com/talos/v1.13/platform-specific-installations/bare-metal-platforms/metal-network-configuration Compile: @@ -17,4 +17,4 @@ talos-meta-tool -device /dev/sda -config config.yaml The `-device` flag accepts the full disk (e.g. `/dev/sda`); the META partition is discovered automatically via GPT. -The `-config` file is validated against the Talos metal network configuration schema (`network.PlatformConfigSpec`) before writing: unknown fields, malformed addresses and invalid enum values are rejected. +The `-config` file is validated against the Talos v1.13 metal network configuration schema (`network.PlatformConfigSpec`) before writing: unknown fields, malformed addresses and invalid enum values are rejected. diff --git a/validate.go b/validate.go index 6e6b769..36aa4cd 100644 --- a/validate.go +++ b/validate.go @@ -13,7 +13,7 @@ import ( // configuration (the format Talos reads from META key 0x0a), rejecting // unknown fields, malformed values and extra YAML documents. // -// See https://docs.siderolabs.com/talos/latest/networking/metal-network-configuration. +// See https://docs.siderolabs.com/talos/v1.13/platform-specific-installations/bare-metal-platforms/metal-network-configuration. func validateConfig(data []byte) error { dec := yaml.NewDecoder(bytes.NewReader(data)) dec.KnownFields(true) diff --git a/validate_test.go b/validate_test.go index 7e98915..ef13d24 100644 --- a/validate_test.go +++ b/validate_test.go @@ -6,7 +6,7 @@ import ( ) // validNetworkConfig follows the metal network configuration format, -// see https://docs.siderolabs.com/talos/latest/networking/metal-network-configuration. +// see https://docs.siderolabs.com/talos/v1.13/platform-specific-installations/bare-metal-platforms/metal-network-configuration. const validNetworkConfig = `addresses: - address: 147.75.61.43/31 linkName: bond0 From 0d103538a728c3f5bb40e88282cc1d667339183a Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 4 Jun 2026 20:57:01 +0200 Subject: [PATCH 3/4] fix: preserve parser error for malformed trailing YAML documents Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- validate.go | 6 +++++- validate_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/validate.go b/validate.go index 36aa4cd..e60c2c3 100644 --- a/validate.go +++ b/validate.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "fmt" "io" "github.com/siderolabs/talos/pkg/machinery/resources/network" @@ -28,8 +29,11 @@ func validateConfig(data []byte) error { return err } - if err := dec.Decode(new(any)); !errors.Is(err, io.EOF) { + switch err := dec.Decode(new(any)); { + case err == nil: return errors.New("unexpected extra YAML document") + case !errors.Is(err, io.EOF): + return fmt.Errorf("unexpected extra YAML document: %w", err) } return nil diff --git a/validate_test.go b/validate_test.go index ef13d24..9236076 100644 --- a/validate_test.go +++ b/validate_test.go @@ -83,6 +83,7 @@ func TestValidateConfigInvalid(t *testing.T) { {"bad enum value", "addresses:\n - address: 1.2.3.4/32\n family: inet5\n"}, {"bad IP", "externalIPs:\n - not-an-ip\n"}, {"extra document", validNetworkConfig + "---\nexternalIPs: []\n"}, + {"malformed extra document", validNetworkConfig + "---\nkey: [\ninvalid"}, } { t.Run(tt.name, func(t *testing.T) { if err := validateConfig([]byte(tt.config)); err == nil { @@ -102,3 +103,15 @@ func TestValidateConfigUnknownFieldError(t *testing.T) { t.Fatalf("error should mention the unknown field, got: %v", err) } } + +func TestValidateConfigMalformedExtraDocumentError(t *testing.T) { + err := validateConfig([]byte(validNetworkConfig + "---\nkey: [\ninvalid")) + if err == nil { + t.Fatal("expected error, got nil") + } + + // The underlying parser error must be preserved for diagnostics. + if !strings.Contains(err.Error(), "yaml") { + t.Fatalf("error should preserve the parser error, got: %v", err) + } +} From 7e26fdc6b2c8901f42d7f78d26b878a4921405fb Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Fri, 5 Jun 2026 10:45:25 +0200 Subject: [PATCH 4/4] feat: add -skip-validation flag Allow bypassing the schema check so a user on a newer Talos version (with a config that uses fields this tool's schema does not yet know about) can still write META without waiting for the tool to bump its machinery dependency. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- README.md | 2 ++ main.go | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a55eba..6a56bb7 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,5 @@ talos-meta-tool -device /dev/sda -config config.yaml The `-device` flag accepts the full disk (e.g. `/dev/sda`); the META partition is discovered automatically via GPT. The `-config` file is validated against the Talos v1.13 metal network configuration schema (`network.PlatformConfigSpec`) before writing: unknown fields, malformed addresses and invalid enum values are rejected. + +To bypass validation — e.g. when the config uses fields that a newer Talos version has added but this tool's schema does not yet know about — pass `-skip-validation`. diff --git a/main.go b/main.go index 5e57ce1..6c818f1 100644 --- a/main.go +++ b/main.go @@ -90,6 +90,7 @@ func writeConfig(dev interface{ io.ReaderAt; io.WriterAt }, configData []byte) e func main() { devicePath := flag.String("device", "", "Path to the disk device (e.g., /dev/sda)") configPath := flag.String("config", "", "Path to the configuration file (e.g., config.yaml)") + skipValidation := flag.Bool("skip-validation", false, "Skip schema validation of the configuration file") flag.Parse() if *devicePath == "" || *configPath == "" { @@ -102,8 +103,10 @@ func main() { log.Fatalf("Error reading configuration file: %v", err) } - if err := validateConfig(configData); err != nil { - log.Fatalf("Invalid network configuration: %v", err) + if !*skipValidation { + if err := validateConfig(configData); err != nil { + log.Fatalf("Invalid network configuration: %v", err) + } } device, err := os.OpenFile(*devicePath, os.O_RDWR, 0)