diff --git a/cmd/talosctl/cmd/mgmt/cluster/internal/firewallpatch/firewallpatch.go b/cmd/talosctl/cmd/mgmt/cluster/internal/firewallpatch/firewallpatch.go index 0a617a030e..5e3c8de224 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/internal/firewallpatch/firewallpatch.go +++ b/cmd/talosctl/cmd/mgmt/cluster/internal/firewallpatch/firewallpatch.go @@ -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) } diff --git a/pkg/machinery/config/configloader/internal/decoder/decoder.go b/pkg/machinery/config/configloader/internal/decoder/decoder.go index bbfa712c7a..662bb0b829 100644 --- a/pkg/machinery/config/configloader/internal/decoder/decoder.go +++ b/pkg/machinery/config/configloader/internal/decoder/decoder.go @@ -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 "" +} diff --git a/pkg/machinery/config/configloader/internal/decoder/decoder_test.go b/pkg/machinery/config/configloader/internal/decoder/decoder_test.go index 5502b3384e..f01885cb99 100644 --- a/pkg/machinery/config/configloader/internal/decoder/decoder_test.go +++ b/pkg/machinery/config/configloader/internal/decoder/decoder_test.go @@ -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() diff --git a/pkg/machinery/config/configloader/internal/decoder/testdata/double/v1alpha1.yaml b/pkg/machinery/config/configloader/internal/decoder/testdata/double/v1alpha1.yaml new file mode 100644 index 0000000000..c6b585f290 --- /dev/null +++ b/pkg/machinery/config/configloader/internal/decoder/testdata/double/v1alpha1.yaml @@ -0,0 +1,7 @@ +version: v1alpha1 +machine: + type: controlplane +--- +version: v1alpha1 +machine: + type: worker diff --git a/pkg/machinery/config/configpatcher/load.go b/pkg/machinery/config/configpatcher/load.go index 6e4a7199c1..486de89ef2 100644 --- a/pkg/machinery/config/configpatcher/load.go +++ b/pkg/machinery/config/configpatcher/load.go @@ -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 ( diff --git a/pkg/machinery/config/configpatcher/load_test.go b/pkg/machinery/config/configpatcher/load_test.go index b5a23f92c3..6fcc176e04 100644 --- a/pkg/machinery/config/configpatcher/load_test.go +++ b/pkg/machinery/config/configpatcher/load_test.go @@ -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]) } diff --git a/pkg/machinery/config/configpatcher/strategic.go b/pkg/machinery/config/configpatcher/strategic.go index cd897342cd..9ceb9c7622 100644 --- a/pkg/machinery/config/configpatcher/strategic.go +++ b/pkg/machinery/config/configpatcher/strategic.go @@ -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{} diff --git a/pkg/machinery/config/container/container.go b/pkg/machinery/config/container/container.go index bbd9ea5a99..5557ae6fa5 100644 --- a/pkg/machinery/config/container/container.go +++ b/pkg/machinery/config/container/container.go @@ -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)