Skip to content

Commit

Permalink
feat: implement "$patch: delete" logic
Browse files Browse the repository at this point in the history
This PR implements "delete patches", same as in k8s.

Signed-off-by: Dmitriy Matrenichev <[email protected]>
  • Loading branch information
DmitriyMV committed Sep 9, 2024
1 parent 545f75f commit 899f1b9
Show file tree
Hide file tree
Showing 22 changed files with 1,184 additions and 48 deletions.
7 changes: 7 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ Extra announced endpoints can be added using the [`KubespanEndpointsConfig` docu
title = "Machine Configuration via Kernel Command Line"
description = """\
Talos Linux supports supplying zstd-compressed, base64-encoded machine configuration small documents via the kernel command line parameter `talos.config.inline`.
"""

[notes.patch-delete]
title = "Removing parts of the configuration using `$patch: delete` syntax"
description = """\
Talos Linux now supports removing parts of the configuration using the `$patch: delete` syntax similar to the kubernetes.
More information can be found [here](https://www.talos.dev/v1.8/talos-guides/configuration/patching/#strategic-merge-patches).
"""

[make_deps]
Expand Down
38 changes: 34 additions & 4 deletions pkg/machinery/config/configloader/configloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ import (
var ErrNoConfig = errors.New("config not found")

// newConfig initializes and returns a Configurator.
func newConfig(r io.Reader) (config config.Provider, err error) {
func newConfig(r io.Reader, opt ...Opt) (config config.Provider, err error) {
var opts Opts

for _, o := range opt {
o(&opts)
}

dec := decoder.NewDecoder()

var buf bytes.Buffer

// preserve the original contents
r = io.TeeReader(r, &buf)

manifests, err := dec.Decode(r)
manifests, err := dec.Decode(r, opts.allowPatchDelete)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -59,6 +65,30 @@ func NewFromStdin() (config.Provider, error) {
}

// NewFromBytes will take a byteslice and attempt to parse a config file from it.
func NewFromBytes(source []byte) (config.Provider, error) {
return newConfig(bytes.NewReader(source))
func NewFromBytes(source []byte, o ...Opt) (config.Provider, error) {
return newConfig(bytes.NewReader(source), o...)
}

// Opts represents the options for the config loader.
type Opts struct {
allowPatchDelete bool
}

// Opt is a functional option for the config loader.
type Opt func(*Opts)

// WithAllowPatchDelete allows the loader to parse patch delete operations.
func WithAllowPatchDelete() Opt {
return func(o *Opts) {
o.allowPatchDelete = true
}
}

// Selector represents a delete selector for a document.
type Selector = decoder.Selector

// ErrZeroedDocument is returned when the document is empty after applying the delete selector.
var ErrZeroedDocument = decoder.ErrZeroedDocument

// ErrLookupFailed is returned when the lookup failed.
var ErrLookupFailed = decoder.ErrLookupFailed
41 changes: 16 additions & 25 deletions pkg/machinery/config/configloader/internal/decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const (
type Decoder struct{}

// Decode decodes all known manifests.
func (d *Decoder) Decode(r io.Reader) ([]config.Document, error) {
return parse(r)
func (d *Decoder) Decode(r io.Reader, allowPatchDelete bool) ([]config.Document, error) {
return parse(r, allowPatchDelete)
}

// NewDecoder initializes and returns a `Decoder`.
Expand All @@ -54,7 +54,8 @@ type documentID struct {
Name string
}

func parse(r io.Reader) (decoded []config.Document, err error) {
//nolint:gocyclo
func parse(r io.Reader, allowPatchDelete bool) (decoded []config.Document, err error) {
// Recover from yaml.v3 panics because we rely on machine configuration loading _a lot_.
defer func() {
if p := recover(); p != nil {
Expand All @@ -71,7 +72,7 @@ func parse(r io.Reader) (decoded []config.Document, err error) {
knownDocuments := map[documentID]struct{}{}

// Iterate through all defined documents.
for {
for i := 0; ; i++ {
var manifests yaml.Node

if err = dec.Decode(&manifests); err != nil {
Expand All @@ -86,6 +87,17 @@ func parse(r io.Reader) (decoded []config.Document, err error) {
return nil, errors.New("expected a document")
}

if allowPatchDelete {
decoded, err = AppendDeletesTo(&manifests, decoded, i)
if err != nil {
return nil, err
}

if manifests.IsZero() {
continue
}
}

for _, manifest := range manifests.Content {
id := documentID{
APIVersion: findValue(manifest, ManifestAPIVersionKey, false),
Expand Down Expand Up @@ -167,24 +179,3 @@ 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 ""
}
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ config:
t.Parallel()

d := decoder.NewDecoder()
actual, err := d.Decode(bytes.NewReader(tt.source))
actual, err := d.Decode(bytes.NewReader(tt.source), false)

if tt.expected != nil {
assert.Equal(t, tt.expected, actual)
Expand Down Expand Up @@ -340,7 +340,7 @@ func TestDecoderV1Alpha1Config(t *testing.T) {
require.NoError(t, err)

d := decoder.NewDecoder()
_, err = d.Decode(bytes.NewReader(contents))
_, err = d.Decode(bytes.NewReader(contents), false)

assert.NoError(t, err)
})
Expand All @@ -354,7 +354,7 @@ func TestDoubleV1Alpha1(t *testing.T) {
contents := must.Value(files.ReadFile("v1alpha1.yaml"))(t)

d := decoder.NewDecoder()
_, err := d.Decode(bytes.NewReader(contents))
_, err := d.Decode(bytes.NewReader(contents), false)
require.Error(t, err)
require.ErrorContains(t, err, "not allowed")
}
Expand All @@ -367,7 +367,7 @@ func BenchmarkDecoderV1Alpha1Config(b *testing.B) {

for range b.N {
d := decoder.NewDecoder()
_, err = d.Decode(bytes.NewReader(contents))
_, err = d.Decode(bytes.NewReader(contents), false)

assert.NoError(b, err)
}
Expand Down
Loading

0 comments on commit 899f1b9

Please sign in to comment.