feat: implement "$patch: delete" logic
This PR implements "delete patches", same as in k8s. Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
This commit is contained in:
parent
545f75fd7a
commit
899f1b9004
@ -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]
|
||||
|
@ -21,7 +21,13 @@ 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
|
||||
@ -29,7 +35,7 @@ func newConfig(r io.Reader) (config config.Provider, err error) {
|
||||
// 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
|
||||
}
|
||||
@ -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
|
||||
|
@ -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`.
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
@ -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),
|
||||
@ -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 ""
|
||||
}
|
||||
|
@ -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)
|
||||
@ -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)
|
||||
})
|
||||
@ -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")
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
234
pkg/machinery/config/configloader/internal/decoder/delete.go
Normal file
234
pkg/machinery/config/configloader/internal/decoder/delete.go
Normal file
@ -0,0 +1,234 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
|
||||
)
|
||||
|
||||
// AppendDeletesTo appends all delete selectors found in the given YAML node to the given destination slice.
|
||||
func AppendDeletesTo(n *yaml.Node, dest []config.Document, idx int) (_ []config.Document, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if re, ok := r.(error); ok {
|
||||
err = re
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
allDeletes(n)(func(path []string, elem delElem) bool {
|
||||
switch elem.parent.Kind {
|
||||
case yaml.DocumentNode:
|
||||
dest = append(dest, makeSelector(path, n.Content[0], idx, "", ""))
|
||||
case yaml.MappingNode:
|
||||
dest = append(dest, makeSelector(path, n.Content[0], idx, "", ""))
|
||||
case yaml.SequenceNode:
|
||||
dest = append(dest, makeSequenceSelector(path, n.Content[0], elem.node, idx))
|
||||
case yaml.ScalarNode, yaml.AliasNode:
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
func allDeletes(node *yaml.Node) func(yield func([]string, delElem) bool) {
|
||||
return func(yield func([]string, delElem) bool) {
|
||||
_, okToDel := processNode(nil, node, make([]string, 0, 8), yield)
|
||||
if okToDel {
|
||||
*node = yaml.Node{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeSequenceSelector(path []string, root, node *yaml.Node, i int) Selector {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
panic(errors.New("expected a mapping node"))
|
||||
}
|
||||
|
||||
// map node inside sequence node, collect the first key:val aside from $patch:delete as selector
|
||||
for j := 0; j < len(node.Content)-1; j += 2 {
|
||||
key := node.Content[j]
|
||||
val := node.Content[j+1]
|
||||
|
||||
if val.Kind == yaml.ScalarNode && key.Value == "$patch" && val.Value == "delete" {
|
||||
continue
|
||||
}
|
||||
|
||||
return makeSelector(path, root, i, key.Value, val.Value)
|
||||
}
|
||||
|
||||
panic(errors.New("no key:val found in sequence node for path " + strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
func makeSelector(path []string, root *yaml.Node, i int, key, val string) Selector {
|
||||
isRequired := len(path) == 0
|
||||
|
||||
apiVersion := findValue(root, "apiVersion", isRequired)
|
||||
kind := findValue(root, "kind", isRequired)
|
||||
|
||||
switch {
|
||||
case kind == "" && apiVersion == "":
|
||||
kind = v1alpha1.Version // legacy document
|
||||
case kind != "" && apiVersion != "":
|
||||
default:
|
||||
panic(fmt.Errorf("kind and apiVersion must be both set for path %s", strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
sel := selector{
|
||||
path: slices.Clone(path),
|
||||
docIdx: i,
|
||||
docAPIVersion: apiVersion,
|
||||
docKind: kind,
|
||||
key: key,
|
||||
value: val,
|
||||
}
|
||||
|
||||
switch name := findValue(root, "name", false); name {
|
||||
case "":
|
||||
return &sel
|
||||
default:
|
||||
return &namedSelector{
|
||||
selector: sel,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type delElem struct {
|
||||
path []string
|
||||
parent, node *yaml.Node
|
||||
}
|
||||
|
||||
// processNode recursively processes a YAML node, searching for a "$patch: delete" nodes and calling the yield function
|
||||
// with path for each one found.
|
||||
//
|
||||
//nolint:gocyclo,cyclop
|
||||
func processNode(
|
||||
parent, v *yaml.Node,
|
||||
path []string,
|
||||
yield func(path []string, d delElem) bool,
|
||||
) (bool, bool) {
|
||||
if v.Kind != yaml.DocumentNode && parent == nil {
|
||||
panic(errors.New("parent must be non-nil for non-document nodes"))
|
||||
}
|
||||
|
||||
switch v.Kind {
|
||||
case yaml.DocumentNode:
|
||||
okToCont, okToDel := processNode(v, v.Content[0], path, yield)
|
||||
|
||||
switch {
|
||||
case !okToCont:
|
||||
return false, okToDel
|
||||
case okToDel:
|
||||
return false, true
|
||||
default:
|
||||
return false, isEmptyDoc(v.Content[0])
|
||||
}
|
||||
|
||||
case yaml.MappingNode:
|
||||
for i := 0; i < len(v.Content)-1; i += 2 {
|
||||
keyNode := v.Content[i]
|
||||
valueNode := v.Content[i+1]
|
||||
|
||||
if valueNode.Kind == yaml.ScalarNode && keyNode.Value == "$patch" && valueNode.Value == "delete" {
|
||||
if parent.Kind != yaml.SequenceNode {
|
||||
ensureNoSeqInChain(path)
|
||||
}
|
||||
|
||||
return yield(path, delElem{path: path, parent: parent, node: v}), true
|
||||
}
|
||||
|
||||
okToCont, okToDel := processNode(v, valueNode, append(path, keyNode.Value), yield)
|
||||
if !okToCont {
|
||||
return false, okToDel
|
||||
} else if okToDel {
|
||||
v.Content = slices.Delete(v.Content, i, i+2)
|
||||
i -= 2
|
||||
|
||||
if len(v.Content) == 0 {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
for i := 0; i < len(v.Content); i++ {
|
||||
okToCont, okToDel := processNode(v, v.Content[i], append(path, "["+strconv.Itoa(i)+"]"), yield)
|
||||
if !okToCont {
|
||||
return false, okToDel
|
||||
} else if okToDel {
|
||||
v.Content = slices.Delete(v.Content, i, i+1)
|
||||
i--
|
||||
|
||||
if len(v.Content) == 0 {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
}
|
||||
case yaml.ScalarNode, yaml.AliasNode:
|
||||
}
|
||||
|
||||
return true, false
|
||||
}
|
||||
|
||||
func isEmptyDoc(node *yaml.Node) bool {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(node.Content)-1; i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
val := node.Content[i+1]
|
||||
|
||||
if keyNode.Kind != yaml.ScalarNode || val.Kind != yaml.ScalarNode {
|
||||
return false
|
||||
}
|
||||
|
||||
if keyNode.Value != "version" && keyNode.Value != "kind" && keyNode.Value != "name" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ensureNoSeqInChain(path []string) {
|
||||
for _, p := range path {
|
||||
if p[0] == '[' {
|
||||
panic(errors.New("cannot delete an inner key in '" + strings.Join(path, ".") + "'"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 in document for which $patch: delete is used", key))
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package decoder_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
"github.com/siderolabs/gen/xtesting/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/configloader/internal/decoder"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed testdata/delete/delete.yaml
|
||||
patchDelete []byte
|
||||
//go:embed testdata/delete/delete_expected.yaml
|
||||
patchDeleteExpected []byte
|
||||
)
|
||||
|
||||
func TestExtractDeletes(t *testing.T) {
|
||||
result, b := must.Values(extractDeletes(patchDelete))(t)
|
||||
|
||||
defer func() {
|
||||
if !t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, sel := range result {
|
||||
t.Logf("%#v", sel)
|
||||
}
|
||||
}()
|
||||
|
||||
require.Equal(t, string(patchDeleteExpected), string(b))
|
||||
|
||||
expected := strings.Join(
|
||||
[]string{
|
||||
"{apiVersion:v1alpha1, kind:SideroLinkConfig, idx:0}",
|
||||
"{path:configFiles.[0], apiVersion:v1alpha1, kind:ExtensionServiceConfig, key:content, value:hello, idx:1, name:foo}",
|
||||
"{path:machine.hostname, kind:v1alpha1, idx:2}",
|
||||
"{path:machine.network.[0], kind:v1alpha1, key:interface, value:eth0, idx:2}",
|
||||
},
|
||||
"\n",
|
||||
)
|
||||
|
||||
actual := strings.Join(
|
||||
xslices.Map(result, func(sel config.Document) string { return sel.(fmt.Stringer).String() }),
|
||||
"\n",
|
||||
)
|
||||
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func extractDeletes(in []byte) (result []config.Document, _ []byte, err error) {
|
||||
var cleanedBytes [][]byte
|
||||
|
||||
dec := yaml.NewDecoder(bytes.NewReader(in))
|
||||
|
||||
for i := 0; ; i++ {
|
||||
node := &yaml.Node{}
|
||||
|
||||
err = dec.Decode(node)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
result, err = decoder.AppendDeletesTo(node, result, i)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !node.IsZero() {
|
||||
b, err := encoder.NewEncoder(node, encoder.WithComments(encoder.CommentsDisabled)).Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cleanedBytes = append(cleanedBytes, b)
|
||||
}
|
||||
}
|
||||
|
||||
return result, bytes.Join(cleanedBytes, []byte("---\n")), nil
|
||||
}
|
292
pkg/machinery/config/configloader/internal/decoder/selector.go
Normal file
292
pkg/machinery/config/configloader/internal/decoder/selector.go
Normal file
@ -0,0 +1,292 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package decoder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/siderolabs/go-pointer"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
)
|
||||
|
||||
// Selector represents a delete selector for a document.
|
||||
type Selector interface {
|
||||
config.Document
|
||||
DocIdx() int
|
||||
ApplyTo(config.Document) error
|
||||
}
|
||||
|
||||
type selector struct {
|
||||
path []string
|
||||
docIdx int
|
||||
docAPIVersion string
|
||||
docKind string
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func (s *selector) Kind() string { return s.docKind }
|
||||
func (s *selector) APIVersion() string { return s.docAPIVersion }
|
||||
func (s *selector) Clone() config.Document { return pointer.To(s.clone()) }
|
||||
func (s *selector) DocIdx() int { return s.docIdx }
|
||||
|
||||
func (s *selector) PathAsString() string { return strings.Join(s.path, ".") }
|
||||
|
||||
func (s *selector) clone() selector {
|
||||
return selector{
|
||||
path: slices.Clone(s.path),
|
||||
docIdx: s.docIdx,
|
||||
docAPIVersion: s.docAPIVersion,
|
||||
docKind: s.docKind,
|
||||
key: s.key,
|
||||
value: s.value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *selector) String() string { return s.toString("") }
|
||||
|
||||
func (s *selector) toString(more string) string {
|
||||
var builder strings.Builder
|
||||
|
||||
writeThing := func(key, val string) {
|
||||
if val != "" {
|
||||
if builder.Len() > 1 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
|
||||
builder.WriteString(key)
|
||||
builder.WriteRune(':')
|
||||
builder.WriteString(val)
|
||||
}
|
||||
}
|
||||
|
||||
builder.WriteRune('{')
|
||||
writeThing("path", s.PathAsString())
|
||||
writeThing("apiVersion", s.docAPIVersion)
|
||||
writeThing("kind", s.docKind)
|
||||
writeThing("key", s.key)
|
||||
writeThing("value", s.value)
|
||||
writeThing("idx", strconv.Itoa(s.docIdx))
|
||||
|
||||
if more != "" {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(more)
|
||||
}
|
||||
|
||||
builder.WriteRune('}')
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// ErrZeroedDocument is returned when the document is empty after applying the delete selector.
|
||||
var ErrZeroedDocument = errors.New("document is empty now")
|
||||
|
||||
// ApplyTo applies the delete selector to the given document.
|
||||
func (s *selector) ApplyTo(doc config.Document) error {
|
||||
if err := s.applyTo(doc); err != nil {
|
||||
return fmt.Errorf("patch delete: path '%s' in document '%s/%s': %w", s.PathAsString(), doc.APIVersion(), doc.Kind(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *selector) applyTo(doc config.Document) error {
|
||||
if s.docKind != doc.Kind() || s.docAPIVersion != doc.APIVersion() {
|
||||
return fmt.Errorf(
|
||||
"incorrect document type for %s/%s",
|
||||
s.docAPIVersion,
|
||||
s.docKind,
|
||||
)
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(doc)
|
||||
|
||||
if val.Kind() != reflect.Pointer {
|
||||
return fmt.Errorf("document type is not a pointer")
|
||||
}
|
||||
|
||||
if len(s.path) == 0 {
|
||||
if doc.Kind() == "" {
|
||||
return errors.New("can't delete the root of the legacy document")
|
||||
}
|
||||
|
||||
return ErrZeroedDocument
|
||||
}
|
||||
|
||||
err := deleteForPath(val.Elem(), s.path, s.key, s.value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete path '%s': %w", s.PathAsString(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var searchForType = reflect.TypeFor[string]()
|
||||
|
||||
// ErrLookupFailed is returned when the lookup failed.
|
||||
var ErrLookupFailed = errors.New("lookup failed")
|
||||
|
||||
//nolint:gocyclo
|
||||
func deleteForPath(val reflect.Value, path []string, key, value string) error {
|
||||
if len(path) == 0 {
|
||||
return errors.New("path is empty")
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Pointer || val.Kind() == reflect.Interface {
|
||||
return deleteForPath(val.Elem(), path, key, value)
|
||||
}
|
||||
|
||||
searchFor := path[0]
|
||||
path = path[1:]
|
||||
valType := val.Type()
|
||||
|
||||
switch val.Kind() { //nolint:exhaustive
|
||||
case reflect.Struct:
|
||||
// Lookup using yaml tag
|
||||
for i := range val.NumField() {
|
||||
structField := valType.Field(i)
|
||||
|
||||
yamlTagRaw, ok := structField.Tag.Lookup("yaml")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
yamlTags := strings.Split(yamlTagRaw, ",")
|
||||
if yamlTags[0] == searchFor {
|
||||
if len(path) == 0 {
|
||||
val.Field(i).SetZero()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return deleteForPath(val.Field(i), path, key, value)
|
||||
}
|
||||
}
|
||||
case reflect.Map:
|
||||
if val.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
keyType := valType.Key()
|
||||
|
||||
// Try assingable and convertible types for key search
|
||||
if searchForType.AssignableTo(keyType) || searchForType.ConvertibleTo(keyType) {
|
||||
searchForVal := reflect.ValueOf(searchFor)
|
||||
|
||||
if searchForType != keyType {
|
||||
searchForVal = searchForVal.Convert(keyType)
|
||||
}
|
||||
|
||||
if idx := val.MapIndex(searchForVal); idx.IsValid() {
|
||||
if len(path) == 0 {
|
||||
val.SetMapIndex(searchForVal, reflect.Zero(valType.Elem()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return deleteForPath(idx, path, key, value)
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
return deleteStructFrom(val, searchFor, path, key, value)
|
||||
}
|
||||
|
||||
return ErrLookupFailed
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func deleteStructFrom(searchIn reflect.Value, searchFor string, path []string, key, value string) error {
|
||||
switch {
|
||||
case len(path) != 0:
|
||||
return errors.New("searching for complex paths in slices is not supported")
|
||||
case searchFor == "":
|
||||
return errors.New("searching for '' in a slice is not supported")
|
||||
case searchFor[0] != '[':
|
||||
return errors.New("searching for non-integer keys in slices is not supported")
|
||||
case searchIn.Kind() != reflect.Slice:
|
||||
return errors.New("searching for a key in a non-slice")
|
||||
}
|
||||
|
||||
for i := 0; i < searchIn.Len(); i++ { //nolint:intrange
|
||||
elem := searchIn.Index(i)
|
||||
|
||||
for elem.Kind() == reflect.Pointer {
|
||||
elem = elem.Elem()
|
||||
}
|
||||
|
||||
if elem.Kind() != reflect.Struct && elem.Kind() != reflect.Map {
|
||||
continue
|
||||
}
|
||||
|
||||
elemType := elem.Type()
|
||||
|
||||
if elem.Kind() == reflect.Struct {
|
||||
for j := range elemType.NumField() {
|
||||
structField := elemType.Field(j)
|
||||
|
||||
yamlTagRaw, ok := structField.Tag.Lookup("yaml")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
yamlTags := strings.Split(yamlTagRaw, ",")
|
||||
if yamlTags[0] != key {
|
||||
continue
|
||||
}
|
||||
|
||||
if elem.Field(j).String() != value {
|
||||
continue
|
||||
}
|
||||
|
||||
searchIn.Set(reflect.AppendSlice(searchIn.Slice(0, i), searchIn.Slice(i+1, searchIn.Len())))
|
||||
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return ErrLookupFailed
|
||||
}
|
||||
|
||||
type namedSelector struct {
|
||||
selector
|
||||
name string
|
||||
}
|
||||
|
||||
func (n *namedSelector) Name() string { return n.name }
|
||||
func (n *namedSelector) String() string { return n.toString("name:" + n.name) }
|
||||
func (n *namedSelector) Clone() config.Document {
|
||||
return &namedSelector{selector: n.selector.clone(), name: n.name}
|
||||
}
|
||||
|
||||
// ApplyTo applies the delete selector to the given document.
|
||||
func (n *namedSelector) ApplyTo(doc config.Document) error {
|
||||
if err := n.applyTo(doc); err != nil {
|
||||
return fmt.Errorf("named patch delete: document %s/%s: %w", doc.APIVersion(), doc.Kind(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *namedSelector) applyTo(doc config.Document) error {
|
||||
namedDoc, ok := doc.(config.NamedDocument)
|
||||
if !ok {
|
||||
return errors.New("not a named document, expected " + n.name)
|
||||
}
|
||||
|
||||
if n.name != namedDoc.Name() {
|
||||
return fmt.Errorf("name mismatch, expected %s, got %s", n.name, namedDoc.Name())
|
||||
}
|
||||
|
||||
return n.selector.applyTo(doc)
|
||||
}
|
24
pkg/machinery/config/configloader/internal/decoder/testdata/delete/delete.yaml
vendored
Normal file
24
pkg/machinery/config/configloader/internal/decoder/testdata/delete/delete.yaml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: SideroLinkConfig
|
||||
$patch: delete
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: ExtensionServiceConfig
|
||||
name: foo
|
||||
configFiles:
|
||||
- content: hello
|
||||
$patch: delete
|
||||
- content: hello2
|
||||
mountPath: /etc/foo2
|
||||
---
|
||||
version: v1alpha1
|
||||
machine:
|
||||
hostname:
|
||||
$patch: delete
|
||||
network:
|
||||
- interface: eth0
|
||||
$patch: delete
|
||||
- interface: eth1
|
||||
addresses: [10.3.5.5/32]
|
||||
- interface: eth0
|
||||
dhcp6: true
|
14
pkg/machinery/config/configloader/internal/decoder/testdata/delete/delete_expected.yaml
vendored
Normal file
14
pkg/machinery/config/configloader/internal/decoder/testdata/delete/delete_expected.yaml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: ExtensionServiceConfig
|
||||
name: foo
|
||||
configFiles:
|
||||
- content: hello2
|
||||
mountPath: /etc/foo2
|
||||
---
|
||||
version: v1alpha1
|
||||
machine:
|
||||
network:
|
||||
- interface: eth1
|
||||
addresses: [10.3.5.5/32]
|
||||
- interface: eth0
|
||||
dhcp6: true
|
@ -210,3 +210,85 @@ func TestApplyWithManifestNewline(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed testdata/patchdelete/config.yaml
|
||||
var configMultidocDelete []byte
|
||||
|
||||
//go:embed testdata/patchdelete/expected.yaml
|
||||
var expectedMultidocDelete string
|
||||
|
||||
func TestApplyMultiDocDelete(t *testing.T) {
|
||||
patches, err := configpatcher.LoadPatches([]string{
|
||||
"@testdata/patchdelete/strategic1.yaml",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := configloader.NewFromBytes(configMultidocDelete)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input configpatcher.Input
|
||||
}{
|
||||
{
|
||||
name: "WithConfig",
|
||||
input: configpatcher.WithConfig(cfg),
|
||||
},
|
||||
{
|
||||
name: "WithBytes",
|
||||
input: configpatcher.WithBytes(configMultidocDelete),
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
out, err := configpatcher.Apply(tt.input, patches)
|
||||
require.NoError(t, err)
|
||||
|
||||
bytes, err := out.Bytes()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedMultidocDelete, string(bytes))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed testdata/patchdelete/controlplane_orig.yaml
|
||||
var controlPlane []byte
|
||||
|
||||
//go:embed testdata/patchdelete/controlplane_expected.yaml
|
||||
var controlPlaneExpected string
|
||||
|
||||
func TestApplyMultiDocCPDelete(t *testing.T) {
|
||||
patches, err := configpatcher.LoadPatches([]string{
|
||||
"@testdata/patchdelete/strategic2.yaml",
|
||||
"@testdata/patchdelete/strategic3.yaml",
|
||||
"@testdata/patchdelete/strategic4.yaml",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := configloader.NewFromBytes(controlPlane)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input configpatcher.Input
|
||||
}{
|
||||
{
|
||||
name: "WithConfig",
|
||||
input: configpatcher.WithConfig(cfg),
|
||||
},
|
||||
{
|
||||
name: "WithBytes",
|
||||
input: configpatcher.WithBytes(controlPlane),
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
out, err := configpatcher.Apply(tt.input, patches)
|
||||
require.NoError(t, err)
|
||||
|
||||
bytes, err := out.Bytes()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, controlPlaneExpected, string(bytes))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ 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, as it is more strict about the config format
|
||||
cfg, strategicErr := configloader.NewFromBytes(in)
|
||||
cfg, strategicErr := configloader.NewFromBytes(in, configloader.WithAllowPatchDelete())
|
||||
if strategicErr == nil {
|
||||
return NewStrategicMergePatch(cfg), nil
|
||||
}
|
||||
|
@ -5,10 +5,14 @@
|
||||
package configpatcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/siderolabs/gen/xslices"
|
||||
|
||||
coreconfig "github.com/siderolabs/talos/pkg/machinery/config"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/config"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/configloader"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/container"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/merge"
|
||||
)
|
||||
@ -46,9 +50,26 @@ func StrategicMerge(cfg coreconfig.Provider, patch StrategicMergePatch) (corecon
|
||||
id := documentID(rightDoc)
|
||||
|
||||
if leftDoc, ok := leftIndex[id]; ok {
|
||||
sel, isSel := rightDoc.(configloader.Selector)
|
||||
if !isSel {
|
||||
if err := merge.Merge(leftDoc, rightDoc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
err := sel.ApplyTo(leftDoc)
|
||||
if err != nil {
|
||||
if !errors.Is(err, configloader.ErrZeroedDocument) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
delete(leftIndex, id)
|
||||
|
||||
idx := slices.Index(left, leftDoc)
|
||||
left = slices.Delete(left, idx, idx+1)
|
||||
}
|
||||
} else {
|
||||
left = append(left, rightDoc)
|
||||
}
|
||||
|
22
pkg/machinery/config/configpatcher/testdata/patchdelete/config.yaml
vendored
Normal file
22
pkg/machinery/config/configpatcher/testdata/patchdelete/config.yaml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
version: v1alpha1
|
||||
machine:
|
||||
network:
|
||||
hostname: hostname1
|
||||
interfaces:
|
||||
- interface: eth0
|
||||
dhcp: true
|
||||
- interface: eth1
|
||||
addresses: [10.3.5.4/32]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: SideroLinkConfig
|
||||
apiUrl: https://siderolink.api/join?jointoken=secret&user=alice
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: ExtensionServiceConfig
|
||||
name: foo
|
||||
configFiles:
|
||||
- content: hello
|
||||
mountPath: /etc/foo
|
||||
environment:
|
||||
- FOO=BAR
|
73
pkg/machinery/config/configpatcher/testdata/patchdelete/controlplane_expected.yaml
vendored
Normal file
73
pkg/machinery/config/configpatcher/testdata/patchdelete/controlplane_expected.yaml
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
version: v1alpha1
|
||||
debug: false
|
||||
persist: true
|
||||
machine:
|
||||
type: controlplane
|
||||
token: d8cwfa.eyvpi0xwxyarbfid
|
||||
ca:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJQakNCOGFBREFnRUNBaEI5cStGVXpodzkycHVPemtpNzB1eGRNQVVHQXl0bGNEQVFNUTR3REFZRFZRUUsKRXdWMFlXeHZjekFlRncweU16RXdNVEl4TURRMk1EbGFGdzB6TXpFd01Ea3hNRFEyTURsYU1CQXhEakFNQmdOVgpCQW9UQlhSaGJHOXpNQ293QlFZREsyVndBeUVBaHVLczZxeCtKWi8wWG8ybXdpQUNjK1EwSVYySGhMd3ozVTZICmUxemZjS2lqWVRCZk1BNEdBMVVkRHdFQi93UUVBd0lDaERBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUkKS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVSlgzWlVNRktWWFZ5NWhKWQozZG9NWENpVEJZRXdCUVlESzJWd0EwRUFCbUxrbDhITmQ3cUpEN3VqQkk2UG9abVRQQWlEcU9GQ0NTVDZJYlZDClF3UzQ1bk1tMldtalRIc3ZrYU5FQ0dneTBhQXJaaFdsbnVYWUswY0t3Z2VJQ0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
key: LS0tLS1CRUdJTiBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0KTUM0Q0FRQXdCUVlESzJWd0JDSUVJTURXbklEdVpSdlhQcW1tbSt6bk15SWMrdk53ZjdnYksvSmR3WC9iN2d1RQotLS0tLUVORCBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0K
|
||||
certSANs: []
|
||||
kubelet:
|
||||
image: ghcr.io/siderolabs/kubelet:v1.28.0
|
||||
defaultRuntimeSeccompProfileEnabled: true
|
||||
disableManifestsDirectory: true
|
||||
network: {}
|
||||
install:
|
||||
wipe: false
|
||||
features:
|
||||
rbac: true
|
||||
stableHostname: true
|
||||
apidCheckExtKeyUsage: true
|
||||
diskQuotaSupport: true
|
||||
kubePrism:
|
||||
enabled: true
|
||||
port: 7445
|
||||
hostDNS:
|
||||
enabled: true
|
||||
forwardKubeDNSToHost: true
|
||||
nodeLabels:
|
||||
node.kubernetes.io/exclude-from-external-load-balancers: ""
|
||||
cluster:
|
||||
id: 0raF93qnkMvF-FZNuvyGozXNdLiT2FOWSlyBaW4PR-w=
|
||||
secret: pofHbABZq7VXuObsdLdy/bHmz6hlMHZ3p8+6WKrv1ic=
|
||||
controlPlane:
|
||||
endpoint: https://base:6443
|
||||
clusterName: base
|
||||
network:
|
||||
dnsDomain: cluster.local
|
||||
podSubnets:
|
||||
- 10.244.0.0/16
|
||||
serviceSubnets:
|
||||
- 10.96.0.0/12
|
||||
token: inn7ol.u4ehnti8qyls9ymo
|
||||
secretboxEncryptionSecret: 45yd2Ke+sytiICojDf8aibTfgt99nzJmO53cjDqrCto=
|
||||
ca:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpVENDQVMrZ0F3SUJBZ0lRYm1hNDNPalRwR0I5TjVxOVFEc3RFekFLQmdncWhrak9QUVFEQWpBVk1STXcKRVFZRFZRUUtFd3ByZFdKbGNtNWxkR1Z6TUI0WERUSXpNVEF4TWpFd05EWXdPVm9YRFRNek1UQXdPVEV3TkRZdwpPVm93RlRFVE1CRUdBMVVFQ2hNS2EzVmlaWEp1WlhSbGN6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VICkEwSUFCTXNhRWZ5R3lFb0xyK0p1Wk91dkVVaXVNMStIQjZvZGtSdVV3ZEJ0ODdacDd1SkVoaEFsZitxNFFjT3gKcFRpZnBIRHJBOEFURjNCWUlFRmFXZ0xPTld1allUQmZNQTRHQTFVZER3RUIvd1FFQXdJQ2hEQWRCZ05WSFNVRQpGakFVQmdnckJnRUZCUWNEQVFZSUt3WUJCUVVIQXdJd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFCkZnUVU0ZEVkM1RoVzRKWlVWcXR1OEFZNWx1NUhQeGN3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUpJbkFMb0EKY1VhRUp4VlJ5dkhQenFQcTBvaGJOY2oyT3N2d3VKUFMzSktVQWlCSmhwNGFWMG9zUURRSGJnbjdXUWFYaHZFTwo5bWxTbVRURTAyOXBWb0YyWkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUVZbFloNzVTUTZ6VUJFTUZ6em5pUzZuVVg3Q2VxQ013S3k0RTZHVEVFMGNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFeXhvUi9JYklTZ3V2NG01azY2OFJTSzR6WDRjSHFoMlJHNVRCMEczenRtbnU0a1NHRUNWLwo2cmhCdzdHbE9KK2tjT3NEd0JNWGNGZ2dRVnBhQXM0MWF3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
aggregatorCA:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJYakNDQVFXZ0F3SUJBZ0lRWnNnVDRZZzVxRkNIbS9QTnV5QUVSekFLQmdncWhrak9QUVFEQWpBQU1CNFgKRFRJek1UQXhNakV3TkRZd09Wb1hEVE16TVRBd09URXdORFl3T1Zvd0FEQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxRwpTTTQ5QXdFSEEwSUFCRmQ1eEhFWHhZRndQeTdaWjhmd3FHRGU2YVQ5ZmxNRVlWZENRNDlEaWZobWVteTVDaHZRCnlVRkpZcFM4b21HODVTS1dnOEpFTkoyNnhEdm9WMFBCS2srallUQmZNQTRHQTFVZER3RUIvd1FFQXdJQ2hEQWQKQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSUt3WUJCUVVIQXdJd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZApCZ05WSFE0RUZnUVV4K0xab1FrYjlmOTN0Y0g4NnZjOUc2ZE13T2t3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnClhudDVXdmEzOGtWVTB3NjExMEp4bU43Qm5zcWl2NnNMaXlJNXRUR1BDQk1DSUZDQlJ3RXZSYTNnU3pkdXB6ajcKQVJLV3NlK3V5YW9rMnlNYXZnaUVITWpUCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUlMblhpQ3hOWU1CWHpncjVuYmc3bnVtUWM2UGlHaXdmWUN2eFF3Tlhxc3dvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVjNuRWNSZkZnWEEvTHRsbngvQ29ZTjdwcFAxK1V3UmhWMEpEajBPSitHWjZiTGtLRzlESgpRVWxpbEx5aVliemxJcGFEd2tRMG5ickVPK2hYUThFcVR3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
serviceAccount:
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUlHVElBQjZZUzV0cFcrUnYxeDBPY09Jb1h0SXgzdGZteVFZNGxOWWRCbmpvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFQ3drbVVTUmtrbnlOc0NjTFJNUTlmZWx6cFY0dDdIdlNRcnp6ZGRvK2pWYmlqd2kwVVE1YQp0VW8vZkxQbDlBckVNOHNRWTVOSlgraVdxYjFkQWFXa2VnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
apiServer:
|
||||
image: registry.k8s.io/kube-apiserver:v1.28.0
|
||||
certSANs:
|
||||
- base
|
||||
disablePodSecurityPolicy: true
|
||||
controllerManager:
|
||||
image: registry.k8s.io/kube-controller-manager:v1.28.0
|
||||
proxy:
|
||||
image: registry.k8s.io/kube-proxy:v1.28.0
|
||||
scheduler:
|
||||
image: registry.k8s.io/kube-scheduler:v1.28.0
|
||||
discovery:
|
||||
enabled: true
|
||||
registries:
|
||||
kubernetes:
|
||||
disabled: true
|
||||
service: {}
|
||||
etcd:
|
||||
ca:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJmVENDQVNPZ0F3SUJBZ0lRVkNTWmFQU3Z0TlZTcjYrVkRyUks0akFLQmdncWhrak9QUVFEQWpBUE1RMHcKQ3dZRFZRUUtFd1JsZEdOa01CNFhEVEl6TVRBeE1qRXdORFl3T1ZvWERUTXpNVEF3T1RFd05EWXdPVm93RHpFTgpNQXNHQTFVRUNoTUVaWFJqWkRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQk9wVXN0MHN3MEJZCkFDN0hpTGNrRElvdVdTRVhWTlJVWE42UmNLTWVRQU9VOEhJQkZBaTJlS2Rka2VJOEhZOTJNWTU1U21xQlhNK3cKRTh0RFgyT3kxSk9qWVRCZk1BNEdBMVVkRHdFQi93UUVBd0lDaERBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjRApBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVejVmai9oZTZoUjhMCkFRTU5qTjgxNS8zV3B6d3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdFWWcyTlp3NkExek02eURNWTRHN1JPVkwKc0JOU0VhSDd4VmVSalBSblAvZ0NJUURiYzFMNmI0SkU0MCtuUCtYNG5pZlB0QWp5REhhUzVMS0YzQWZkUkRWdApMUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU03Q2VnMk1GQW5TM3ROMzV6QTc0aFZ3VElkTkthK0ZwUHlYVERCdU4wVFlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFNmxTeTNTekRRRmdBTHNlSXR5UU1paTVaSVJkVTFGUmMzcEZ3b3g1QUE1VHdjZ0VVQ0xaNApwMTJSNGp3ZGozWXhqbmxLYW9GY3o3QVR5ME5mWTdMVWt3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
95
pkg/machinery/config/configpatcher/testdata/patchdelete/controlplane_orig.yaml
vendored
Normal file
95
pkg/machinery/config/configpatcher/testdata/patchdelete/controlplane_orig.yaml
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
version: v1alpha1
|
||||
debug: false
|
||||
persist: true
|
||||
machine:
|
||||
type: controlplane
|
||||
token: d8cwfa.eyvpi0xwxyarbfid
|
||||
ca:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJQakNCOGFBREFnRUNBaEI5cStGVXpodzkycHVPemtpNzB1eGRNQVVHQXl0bGNEQVFNUTR3REFZRFZRUUsKRXdWMFlXeHZjekFlRncweU16RXdNVEl4TURRMk1EbGFGdzB6TXpFd01Ea3hNRFEyTURsYU1CQXhEakFNQmdOVgpCQW9UQlhSaGJHOXpNQ293QlFZREsyVndBeUVBaHVLczZxeCtKWi8wWG8ybXdpQUNjK1EwSVYySGhMd3ozVTZICmUxemZjS2lqWVRCZk1BNEdBMVVkRHdFQi93UUVBd0lDaERBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFRWUkKS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVSlgzWlVNRktWWFZ5NWhKWQozZG9NWENpVEJZRXdCUVlESzJWd0EwRUFCbUxrbDhITmQ3cUpEN3VqQkk2UG9abVRQQWlEcU9GQ0NTVDZJYlZDClF3UzQ1bk1tMldtalRIc3ZrYU5FQ0dneTBhQXJaaFdsbnVYWUswY0t3Z2VJQ0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
key: LS0tLS1CRUdJTiBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0KTUM0Q0FRQXdCUVlESzJWd0JDSUVJTURXbklEdVpSdlhQcW1tbSt6bk15SWMrdk53ZjdnYksvSmR3WC9iN2d1RQotLS0tLUVORCBFRDI1NTE5IFBSSVZBVEUgS0VZLS0tLS0K
|
||||
certSANs: []
|
||||
kubelet:
|
||||
image: ghcr.io/siderolabs/kubelet:v1.28.0
|
||||
defaultRuntimeSeccompProfileEnabled: true
|
||||
disableManifestsDirectory: true
|
||||
network: {}
|
||||
install:
|
||||
wipe: false
|
||||
features:
|
||||
rbac: true
|
||||
stableHostname: true
|
||||
apidCheckExtKeyUsage: true
|
||||
diskQuotaSupport: true
|
||||
kubePrism:
|
||||
enabled: true
|
||||
port: 7445
|
||||
hostDNS:
|
||||
enabled: true
|
||||
forwardKubeDNSToHost: true
|
||||
nodeLabels:
|
||||
node.kubernetes.io/exclude-from-external-load-balancers: ""
|
||||
cluster:
|
||||
id: 0raF93qnkMvF-FZNuvyGozXNdLiT2FOWSlyBaW4PR-w=
|
||||
secret: pofHbABZq7VXuObsdLdy/bHmz6hlMHZ3p8+6WKrv1ic=
|
||||
controlPlane:
|
||||
endpoint: https://base:6443
|
||||
clusterName: base
|
||||
network:
|
||||
dnsDomain: cluster.local
|
||||
podSubnets:
|
||||
- 10.244.0.0/16
|
||||
serviceSubnets:
|
||||
- 10.96.0.0/12
|
||||
token: inn7ol.u4ehnti8qyls9ymo
|
||||
secretboxEncryptionSecret: 45yd2Ke+sytiICojDf8aibTfgt99nzJmO53cjDqrCto=
|
||||
ca:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpVENDQVMrZ0F3SUJBZ0lRYm1hNDNPalRwR0I5TjVxOVFEc3RFekFLQmdncWhrak9QUVFEQWpBVk1STXcKRVFZRFZRUUtFd3ByZFdKbGNtNWxkR1Z6TUI0WERUSXpNVEF4TWpFd05EWXdPVm9YRFRNek1UQXdPVEV3TkRZdwpPVm93RlRFVE1CRUdBMVVFQ2hNS2EzVmlaWEp1WlhSbGN6QlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VICkEwSUFCTXNhRWZ5R3lFb0xyK0p1Wk91dkVVaXVNMStIQjZvZGtSdVV3ZEJ0ODdacDd1SkVoaEFsZitxNFFjT3gKcFRpZnBIRHJBOEFURjNCWUlFRmFXZ0xPTld1allUQmZNQTRHQTFVZER3RUIvd1FFQXdJQ2hEQWRCZ05WSFNVRQpGakFVQmdnckJnRUZCUWNEQVFZSUt3WUJCUVVIQXdJd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFCkZnUVU0ZEVkM1RoVzRKWlVWcXR1OEFZNWx1NUhQeGN3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUpJbkFMb0EKY1VhRUp4VlJ5dkhQenFQcTBvaGJOY2oyT3N2d3VKUFMzSktVQWlCSmhwNGFWMG9zUURRSGJnbjdXUWFYaHZFTwo5bWxTbVRURTAyOXBWb0YyWkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUVZbFloNzVTUTZ6VUJFTUZ6em5pUzZuVVg3Q2VxQ013S3k0RTZHVEVFMGNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFeXhvUi9JYklTZ3V2NG01azY2OFJTSzR6WDRjSHFoMlJHNVRCMEczenRtbnU0a1NHRUNWLwo2cmhCdzdHbE9KK2tjT3NEd0JNWGNGZ2dRVnBhQXM0MWF3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
aggregatorCA:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJYakNDQVFXZ0F3SUJBZ0lRWnNnVDRZZzVxRkNIbS9QTnV5QUVSekFLQmdncWhrak9QUVFEQWpBQU1CNFgKRFRJek1UQXhNakV3TkRZd09Wb1hEVE16TVRBd09URXdORFl3T1Zvd0FEQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxRwpTTTQ5QXdFSEEwSUFCRmQ1eEhFWHhZRndQeTdaWjhmd3FHRGU2YVQ5ZmxNRVlWZENRNDlEaWZobWVteTVDaHZRCnlVRkpZcFM4b21HODVTS1dnOEpFTkoyNnhEdm9WMFBCS2srallUQmZNQTRHQTFVZER3RUIvd1FFQXdJQ2hEQWQKQmdOVkhTVUVGakFVQmdnckJnRUZCUWNEQVFZSUt3WUJCUVVIQXdJd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZApCZ05WSFE0RUZnUVV4K0xab1FrYjlmOTN0Y0g4NnZjOUc2ZE13T2t3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnClhudDVXdmEzOGtWVTB3NjExMEp4bU43Qm5zcWl2NnNMaXlJNXRUR1BDQk1DSUZDQlJ3RXZSYTNnU3pkdXB6ajcKQVJLV3NlK3V5YW9rMnlNYXZnaUVITWpUCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUlMblhpQ3hOWU1CWHpncjVuYmc3bnVtUWM2UGlHaXdmWUN2eFF3Tlhxc3dvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVjNuRWNSZkZnWEEvTHRsbngvQ29ZTjdwcFAxK1V3UmhWMEpEajBPSitHWjZiTGtLRzlESgpRVWxpbEx5aVliemxJcGFEd2tRMG5ickVPK2hYUThFcVR3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
serviceAccount:
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUlHVElBQjZZUzV0cFcrUnYxeDBPY09Jb1h0SXgzdGZteVFZNGxOWWRCbmpvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFQ3drbVVTUmtrbnlOc0NjTFJNUTlmZWx6cFY0dDdIdlNRcnp6ZGRvK2pWYmlqd2kwVVE1YQp0VW8vZkxQbDlBckVNOHNRWTVOSlgraVdxYjFkQWFXa2VnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
apiServer:
|
||||
image: registry.k8s.io/kube-apiserver:v1.28.0
|
||||
certSANs:
|
||||
- base
|
||||
disablePodSecurityPolicy: true
|
||||
admissionControl:
|
||||
- name: PodSecurity
|
||||
configuration:
|
||||
apiVersion: pod-security.admission.config.k8s.io/v1alpha1
|
||||
defaults:
|
||||
audit: restricted
|
||||
audit-version: latest
|
||||
enforce: baseline
|
||||
enforce-version: latest
|
||||
warn: restricted
|
||||
warn-version: latest
|
||||
exemptions:
|
||||
namespaces:
|
||||
- kube-system
|
||||
runtimeClasses: []
|
||||
usernames: []
|
||||
kind: PodSecurityConfiguration
|
||||
auditPolicy:
|
||||
apiVersion: audit.k8s.io/v1
|
||||
kind: Policy
|
||||
rules:
|
||||
- level: Metadata
|
||||
controllerManager:
|
||||
image: registry.k8s.io/kube-controller-manager:v1.28.0
|
||||
proxy:
|
||||
image: registry.k8s.io/kube-proxy:v1.28.0
|
||||
scheduler:
|
||||
image: registry.k8s.io/kube-scheduler:v1.28.0
|
||||
discovery:
|
||||
enabled: true
|
||||
registries:
|
||||
kubernetes:
|
||||
disabled: true
|
||||
service: {}
|
||||
etcd:
|
||||
ca:
|
||||
crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJmVENDQVNPZ0F3SUJBZ0lRVkNTWmFQU3Z0TlZTcjYrVkRyUks0akFLQmdncWhrak9QUVFEQWpBUE1RMHcKQ3dZRFZRUUtFd1JsZEdOa01CNFhEVEl6TVRBeE1qRXdORFl3T1ZvWERUTXpNVEF3T1RFd05EWXdPVm93RHpFTgpNQXNHQTFVRUNoTUVaWFJqWkRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQk9wVXN0MHN3MEJZCkFDN0hpTGNrRElvdVdTRVhWTlJVWE42UmNLTWVRQU9VOEhJQkZBaTJlS2Rka2VJOEhZOTJNWTU1U21xQlhNK3cKRTh0RFgyT3kxSk9qWVRCZk1BNEdBMVVkRHdFQi93UUVBd0lDaERBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjRApBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVejVmai9oZTZoUjhMCkFRTU5qTjgxNS8zV3B6d3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdFWWcyTlp3NkExek02eURNWTRHN1JPVkwKc0JOU0VhSDd4VmVSalBSblAvZ0NJUURiYzFMNmI0SkU0MCtuUCtYNG5pZlB0QWp5REhhUzVMS0YzQWZkUkRWdApMUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
||||
key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU03Q2VnMk1GQW5TM3ROMzV6QTc0aFZ3VElkTkthK0ZwUHlYVERCdU4wVFlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFNmxTeTNTekRRRmdBTHNlSXR5UU1paTVaSVJkVTFGUmMzcEZ3b3g1QUE1VHdjZ0VVQ0xaNApwMTJSNGp3ZGozWXhqbmxLYW9GY3o3QVR5ME5mWTdMVWt3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
23
pkg/machinery/config/configpatcher/testdata/patchdelete/expected.yaml
vendored
Normal file
23
pkg/machinery/config/configpatcher/testdata/patchdelete/expected.yaml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
version: v1alpha1
|
||||
machine:
|
||||
type: ""
|
||||
token: ""
|
||||
certSANs: []
|
||||
network:
|
||||
interfaces:
|
||||
- interface: eth1
|
||||
addresses:
|
||||
- 10.3.5.4/32
|
||||
- 10.3.5.5/32
|
||||
- interface: eth0
|
||||
dummy: true
|
||||
cluster: null
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: ExtensionServiceConfig
|
||||
name: foo
|
||||
configFiles:
|
||||
- content: hello2
|
||||
mountPath: /etc/foo2
|
||||
environment:
|
||||
- FOO=BAR
|
25
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic1.yaml
vendored
Normal file
25
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic1.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: SideroLinkConfig
|
||||
$patch: delete
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: ExtensionServiceConfig
|
||||
name: foo
|
||||
configFiles:
|
||||
- content: hello
|
||||
$patch: delete
|
||||
- content: hello2
|
||||
mountPath: /etc/foo2
|
||||
---
|
||||
version: v1alpha1
|
||||
machine:
|
||||
network:
|
||||
hostname:
|
||||
$patch: delete
|
||||
interfaces:
|
||||
- interface: eth0
|
||||
$patch: delete
|
||||
- interface: eth1
|
||||
addresses: [10.3.5.5/32]
|
||||
- interface: eth0
|
||||
dummy: true
|
6
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic2.yaml
vendored
Normal file
6
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic2.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
version: v1alpha1
|
||||
cluster:
|
||||
apiServer:
|
||||
admissionControl:
|
||||
- name: PodSecurity
|
||||
$patch: delete
|
22
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic3.yaml
vendored
Normal file
22
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic3.yaml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
version: v1alpha1
|
||||
cluster:
|
||||
apiServer:
|
||||
admissionControl:
|
||||
- name: PodSecurity2
|
||||
configuration:
|
||||
apiVersion: pod-security.admission.config.k8s.io/v1alpha1
|
||||
defaults:
|
||||
audit: restricted
|
||||
audit-version: latest
|
||||
enforce: baseline
|
||||
enforce-version: latest
|
||||
warn: restricted
|
||||
warn-version: latest
|
||||
exemptions:
|
||||
namespaces:
|
||||
- kube-system
|
||||
runtimeClasses: []
|
||||
usernames: []
|
||||
kind: PodSecurityConfiguration
|
||||
auditPolicy:
|
||||
$patch: delete
|
6
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic4.yaml
vendored
Normal file
6
pkg/machinery/config/configpatcher/testdata/patchdelete/strategic4.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
version: v1alpha1
|
||||
cluster:
|
||||
apiServer:
|
||||
admissionControl:
|
||||
- name: PodSecurity2
|
||||
$patch: delete
|
@ -9,7 +9,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@ -49,6 +48,7 @@ func New(documents ...config.Document) (*Container, error) {
|
||||
|
||||
container.v1alpha1Config = d
|
||||
default:
|
||||
if _, ok := d.(selector); !ok {
|
||||
documentID := d.Kind() + "/"
|
||||
|
||||
if named, ok := d.(config.NamedDocument); ok {
|
||||
@ -60,6 +60,7 @@ func New(documents ...config.Document) (*Container, error) {
|
||||
}
|
||||
|
||||
seenDocuments[documentID] = struct{}{}
|
||||
}
|
||||
|
||||
container.documents = append(container.documents, d)
|
||||
}
|
||||
@ -334,15 +335,34 @@ func (container *Container) RawV1Alpha1() *v1alpha1.Config {
|
||||
//
|
||||
// Documents should not be modified.
|
||||
func (container *Container) Documents() []config.Document {
|
||||
docs := slices.Clone(container.documents)
|
||||
result := make([]config.Document, 0, len(container.documents)+1)
|
||||
|
||||
if container.v1alpha1Config != nil {
|
||||
docs = append([]config.Document{container.v1alpha1Config}, docs...)
|
||||
// first we take deletes for v1alpha1
|
||||
for _, doc := range container.documents {
|
||||
if _, ok := doc.(selector); ok && doc.Kind() == v1alpha1.Version {
|
||||
result = append(result, doc)
|
||||
}
|
||||
}
|
||||
|
||||
return docs
|
||||
// then we take the v1alpha1 config
|
||||
if container.v1alpha1Config != nil {
|
||||
result = append(result, container.v1alpha1Config)
|
||||
}
|
||||
|
||||
// then we take the rest
|
||||
for _, doc := range container.documents {
|
||||
if _, ok := doc.(selector); ok && doc.Kind() == v1alpha1.Version {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, doc)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type selector interface{ ApplyTo(config.Document) error }
|
||||
|
||||
// CompleteForBoot return true if the machine config is enough to proceed with the boot process.
|
||||
func (container *Container) CompleteForBoot() bool {
|
||||
// for now, v1alpha1 config is required
|
||||
|
@ -58,6 +58,57 @@ When patching a [multi-document machine configuration]({{< relref "../../referen
|
||||
- if the patch document doesn't exist in the machine configuration, it is appended to the machine configuration
|
||||
|
||||
The strategic merge patch itself might be a multi-document YAML, and each document will be applied as a patch to the base machine configuration.
|
||||
Keep in mind that you can't patch the same document multiple times with the same patch.
|
||||
|
||||
You can also delete parts from the configuration using `$patch: delete` syntax similar to the
|
||||
[Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md#delete-directive)
|
||||
strategic merge patch.
|
||||
|
||||
For example, with configuration:
|
||||
|
||||
```yaml
|
||||
machine:
|
||||
network:
|
||||
interfaces:
|
||||
- interface: eth0
|
||||
addresses:
|
||||
- 10.0.0.2/24
|
||||
hostname: worker1
|
||||
```
|
||||
|
||||
and patch document:
|
||||
|
||||
```yaml
|
||||
machine:
|
||||
network:
|
||||
interfaces:
|
||||
- interface: eth0
|
||||
$patch: delete
|
||||
hostname: worker1
|
||||
```
|
||||
|
||||
The resulting configuration will be:
|
||||
|
||||
```yaml
|
||||
machine:
|
||||
network:
|
||||
hostname: worker1
|
||||
```
|
||||
|
||||
You can also delete entire docs (but not the main `v1alpha1` configuration!) using this syntax:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
kind: SideroLinkConfig
|
||||
$patch: delete
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: ExtensionServiceConfig
|
||||
name: foo
|
||||
$patch: delete
|
||||
```
|
||||
|
||||
This will remove the documents `SideroLinkConfig` and `ExtensionServiceConfig` with name `foo` from the configuration.
|
||||
|
||||
### RFC6902 (JSON Patches)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user