chore: disallow duplicate documents on decoder level

Required for #9275

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
This commit is contained in:
Dmitriy Matrenichev 2024-09-06 15:23:34 +03:00
parent bcaf63628b
commit cd7c682662
No known key found for this signature in database
GPG Key ID: 94B473337258BFD5
8 changed files with 93 additions and 10 deletions

View File

@ -135,7 +135,7 @@ func ControlPlane(defaultAction nethelpers.DefaultAction, cidrs []netip.Prefix,
panic(err)
}
return configpatcher.StrategicMergePatch{Provider: provider}
return configpatcher.NewStrategicMergePatch(provider)
}
// Worker generates a default firewall for a worker node.
@ -187,5 +187,5 @@ func Worker(defaultAction nethelpers.DefaultAction, cidrs []netip.Prefix, gatewa
panic(err)
}
return configpatcher.StrategicMergePatch{Provider: provider}
return configpatcher.NewStrategicMergePatch(provider)
}

View File

@ -6,6 +6,7 @@
package decoder
import (
"cmp"
"errors"
"fmt"
"io"
@ -47,6 +48,12 @@ func NewDecoder() *Decoder {
return &Decoder{}
}
type documentID struct {
APIVersion string
Kind string
Name string
}
func parse(r io.Reader) (decoded []config.Document, err error) {
// Recover from yaml.v3 panics because we rely on machine configuration loading _a lot_.
defer func() {
@ -61,6 +68,8 @@ func parse(r io.Reader) (decoded []config.Document, err error) {
dec.KnownFields(true)
knownDocuments := map[documentID]struct{}{}
// Iterate through all defined documents.
for {
var manifests yaml.Node
@ -78,6 +87,18 @@ func parse(r io.Reader) (decoded []config.Document, err error) {
}
for _, manifest := range manifests.Content {
id := documentID{
APIVersion: findValue(manifest, ManifestAPIVersionKey, false),
Kind: cmp.Or(findValue(manifest, ManifestKindKey, false), "v1alpha1"),
Name: findValue(manifest, "name", false),
}
if _, ok := knownDocuments[id]; ok {
return nil, fmt.Errorf("duplicate document %s/%s/%s is not allowed", id.APIVersion, id.Kind, id.Name)
}
knownDocuments[id] = struct{}{}
var target config.Document
if target, err = decode(manifest); err != nil {
@ -146,3 +167,24 @@ func decode(manifest *yaml.Node) (target config.Document, err error) {
return target, nil
}
func findValue(node *yaml.Node, key string, required bool) string {
if node.Kind != yaml.MappingNode {
panic(errors.New("expected a mapping node"))
}
for i := 0; i < len(node.Content)-1; i += 2 {
keyNode := node.Content[i]
val := node.Content[i+1]
if keyNode.Kind == yaml.ScalarNode && keyNode.Value == key {
return val.Value
}
}
if required {
panic(fmt.Errorf("missing '%s'", key))
}
return ""
}

View File

@ -6,10 +6,12 @@ package decoder_test
import (
"bytes"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/siderolabs/gen/xtesting/must"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -345,6 +347,18 @@ func TestDecoderV1Alpha1Config(t *testing.T) {
}
}
func TestDoubleV1Alpha1(t *testing.T) {
t.Parallel()
files := os.DirFS("testdata/double").(fs.ReadFileFS) //nolint:errcheck
contents := must.Value(files.ReadFile("v1alpha1.yaml"))(t)
d := decoder.NewDecoder()
_, err := d.Decode(bytes.NewReader(contents))
require.Error(t, err)
require.ErrorContains(t, err, "not allowed")
}
func BenchmarkDecoderV1Alpha1Config(b *testing.B) {
b.ReportAllocs()

View File

@ -0,0 +1,7 @@
version: v1alpha1
machine:
type: controlplane
---
version: v1alpha1
machine:
type: worker

View File

@ -20,10 +20,10 @@ type patch []map[string]any
// LoadPatch loads the strategic merge patch or JSON patch (JSON/YAML for JSON patch).
func LoadPatch(in []byte) (Patch, error) {
// try configloader first, it is more strict about config format
// Try configloader first, as it is more strict about the config format
cfg, strategicErr := configloader.NewFromBytes(in)
if strategicErr == nil {
return StrategicMergePatch{cfg}, nil
return NewStrategicMergePatch(cfg), nil
}
var (

View File

@ -70,7 +70,7 @@ func TestLoadStrategic(t *testing.T) {
p, ok := raw.(configpatcher.StrategicMergePatch)
require.True(t, ok)
assert.Equal(t, "foo.bar", p.Machine().Network().Hostname())
assert.Equal(t, "foo.bar", p.Provider().Machine().Network().Hostname())
}
func TestLoadJSONPatches(t *testing.T) {
@ -106,6 +106,6 @@ func TestLoadMixedPatches(t *testing.T) {
require.Len(t, patchList, 3)
assert.IsType(t, jsonpatch.Patch{}, patchList[0])
assert.IsType(t, configpatcher.StrategicMergePatch{}, patchList[1])
assert.Implements(t, (*configpatcher.StrategicMergePatch)(nil), patchList[1])
assert.IsType(t, jsonpatch.Patch{}, patchList[2])
}

View File

@ -14,8 +14,9 @@ import (
)
// StrategicMergePatch is a strategic merge config patch.
type StrategicMergePatch struct {
coreconfig.Provider
type StrategicMergePatch interface {
Documents() []config.Document
Provider() coreconfig.Provider
}
// StrategicMerge performs strategic merge config patching.
@ -55,3 +56,20 @@ func StrategicMerge(cfg coreconfig.Provider, patch StrategicMergePatch) (corecon
return container.New(left...)
}
// NewStrategicMergePatch creates a new strategic merge patch. deleteSelectors is a list of delete selectors, can be empty.
func NewStrategicMergePatch(cfg coreconfig.Provider) StrategicMergePatch {
return strategicMergePatch{provider: cfg}
}
type strategicMergePatch struct {
provider coreconfig.Provider
}
func (s strategicMergePatch) Documents() []config.Document {
return s.provider.Documents()
}
func (s strategicMergePatch) Provider() coreconfig.Provider { return s.provider }
var _ StrategicMergePatch = strategicMergePatch{}

View File

@ -91,7 +91,9 @@ func NewV1Alpha1(config *v1alpha1.Config) *Container {
// Clone the container.
//
// Cloned container is not readonly.
func (container *Container) Clone() coreconfig.Provider {
func (container *Container) Clone() coreconfig.Provider { return container.clone() }
func (container *Container) clone() *Container {
return &Container{
v1alpha1Config: container.v1alpha1Config.DeepCopy(),
documents: xslices.Map(container.documents, config.Document.Clone),
@ -304,7 +306,7 @@ func (container *Container) Validate(mode validation.RuntimeMode, opt ...validat
// RedactSecrets returns a copy of the Provider with all secrets replaced with the given string.
func (container *Container) RedactSecrets(replacement string) coreconfig.Provider {
clone := container.Clone().(*Container) //nolint:forcetypeassert,errcheck
clone := container.clone()
if clone.v1alpha1Config != nil {
clone.v1alpha1Config.Redact(replacement)