mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-10 15:11:15 +02:00
feat: validate if extra fields present in the decoder
This should address issues when the config is a valid yaml but contains extra fields which may appear there if the indents got messed up somehow for example. Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
This commit is contained in:
parent
5b57a98008
commit
8ddbcc9643
@ -99,6 +99,13 @@ Talos can be configued to use Kubernetes 1.21 or CAPI v0.4.x components can be u
|
|||||||
description = """\
|
description = """\
|
||||||
Added support for Equinix Metal IPs for the Talos virtual (shared) IP (option `equnixMetal` under `vip` in the machine configuration).
|
Added support for Equinix Metal IPs for the Talos virtual (shared) IP (option `equnixMetal` under `vip` in the machine configuration).
|
||||||
Talos automatically re-assigns IP using the Equinix Metal API when leadership changes.
|
Talos automatically re-assigns IP using the Equinix Metal API when leadership changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[notes.configuration]
|
||||||
|
title = "Machine Config Validation"
|
||||||
|
description = """\
|
||||||
|
Unknown keys in the machine config now make the config invalid,
|
||||||
|
so any attempt to apply/edit the configuration with the unknown keys will lead into an error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[make_deps]
|
[make_deps]
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/talos-systems/talos/pkg/machinery/config"
|
"github.com/talos-systems/talos/pkg/machinery/config"
|
||||||
|
"github.com/talos-systems/talos/pkg/machinery/config/encoder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -95,7 +96,7 @@ func parse(source []byte) (decoded []interface{}, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo,cyclop
|
||||||
func decode(manifest *yaml.Node) (target interface{}, err error) {
|
func decode(manifest *yaml.Node) (target interface{}, err error) {
|
||||||
var (
|
var (
|
||||||
version string
|
version string
|
||||||
@ -136,6 +137,10 @@ func decode(manifest *yaml.Node) (target interface{}, err error) {
|
|||||||
return nil, fmt.Errorf("deprecated decode: %w", err)
|
return nil, fmt.Errorf("deprecated decode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = validate(target, manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,5 +169,96 @@ func decode(manifest *yaml.Node) (target interface{}, err error) {
|
|||||||
return nil, fmt.Errorf("spec decode: %w", err)
|
return nil, fmt.Errorf("spec decode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = validate(target, spec); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return target, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func validate(target interface{}, spec *yaml.Node) error {
|
||||||
|
node, err := encoder.NewEncoder(target).Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
src := map[string]interface{}{}
|
||||||
|
dst := map[string]interface{}{}
|
||||||
|
|
||||||
|
err = spec.Decode(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = node.Decode(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkUnknown func(interface{}, interface{}) interface{}
|
||||||
|
|
||||||
|
checkUnknown = func(left interface{}, right interface{}) interface{} {
|
||||||
|
switch v := left.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
r, ok := right.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return "type mismatch"
|
||||||
|
}
|
||||||
|
|
||||||
|
unknownKeys := map[string]interface{}{}
|
||||||
|
|
||||||
|
for key, value := range v {
|
||||||
|
if _, ok := r[key]; !ok {
|
||||||
|
unknownKeys[key] = value
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if d := checkUnknown(value, r[key]); d != nil {
|
||||||
|
unknownKeys[key] = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unknownKeys) > 0 {
|
||||||
|
return unknownKeys
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
r, ok := right.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return "type mismatch"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v) != len(r) {
|
||||||
|
return "slice length differs"
|
||||||
|
}
|
||||||
|
|
||||||
|
var unknownItems []interface{}
|
||||||
|
|
||||||
|
for i, item := range v {
|
||||||
|
if d := checkUnknown(item, r[i]); d != nil {
|
||||||
|
unknownItems = append(unknownItems, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unknownItems) > 0 {
|
||||||
|
return unknownItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
diff := checkUnknown(src, dst)
|
||||||
|
if diff != nil {
|
||||||
|
var data []byte
|
||||||
|
|
||||||
|
if data, err = yaml.Marshal(diff); err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal error summary %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unknown keys found during decoding:\n%s", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -17,8 +17,17 @@ type Mock struct {
|
|||||||
Test bool `yaml:"test"`
|
Test bool `yaml:"test"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MockV2 struct {
|
||||||
|
Slice []Mock `yaml:"slice"`
|
||||||
|
Map map[string]Mock `yaml:"map"`
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
config.Register("mock", func(string) interface{} {
|
config.Register("mock", func(version string) interface{} {
|
||||||
|
if version == "v1alpha2" {
|
||||||
|
return &MockV2{}
|
||||||
|
}
|
||||||
|
|
||||||
return &Mock{}
|
return &Mock{}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -137,6 +146,85 @@ spec:
|
|||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "extra field",
|
||||||
|
fields: fields{
|
||||||
|
source: []byte(`---
|
||||||
|
kind: mock
|
||||||
|
version: v1alpha1
|
||||||
|
spec:
|
||||||
|
test: true
|
||||||
|
extra: fail
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extra fields in map",
|
||||||
|
fields: fields{
|
||||||
|
source: []byte(`---
|
||||||
|
kind: mock
|
||||||
|
version: v1alpha2
|
||||||
|
spec:
|
||||||
|
map:
|
||||||
|
first:
|
||||||
|
test: true
|
||||||
|
extra: me
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extra fields in slice",
|
||||||
|
fields: fields{
|
||||||
|
source: []byte(`---
|
||||||
|
kind: mock
|
||||||
|
version: v1alpha2
|
||||||
|
spec:
|
||||||
|
slice:
|
||||||
|
- test: true
|
||||||
|
not: working
|
||||||
|
more: extra
|
||||||
|
fields: here
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid nested",
|
||||||
|
fields: fields{
|
||||||
|
source: []byte(`---
|
||||||
|
kind: mock
|
||||||
|
version: v1alpha2
|
||||||
|
spec:
|
||||||
|
slice:
|
||||||
|
- test: true
|
||||||
|
map:
|
||||||
|
first:
|
||||||
|
test: true
|
||||||
|
second:
|
||||||
|
test: false
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
want: []interface{}{
|
||||||
|
&MockV2{
|
||||||
|
Map: map[string]Mock{
|
||||||
|
"first": {
|
||||||
|
Test: true,
|
||||||
|
},
|
||||||
|
"second": {
|
||||||
|
Test: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Slice: []Mock{
|
||||||
|
{Test: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user