Skip to content

Commit

Permalink
feat: expose strategic merge config patches
Browse files Browse the repository at this point in the history
The end result is that every Talos CLI accepts both JSON and strategic
patches to patch machine configuration.

Signed-off-by: Andrey Smirnov <[email protected]>
  • Loading branch information
smira committed Jul 12, 2022
1 parent 6e3d2d6 commit 641f6a1
Show file tree
Hide file tree
Showing 29 changed files with 841 additions and 113 deletions.
15 changes: 7 additions & 8 deletions cmd/talosctl/cmd/mgmt/cluster/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"time"

humanize "github.com/dustin/go-humanize"
jsonpatch "github.com/evanphx/json-patch"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/talos-systems/go-blockdevice/blockdevice/encryption"
Expand Down Expand Up @@ -503,28 +502,28 @@ func create(ctx context.Context, flags *pflag.FlagSet) (err error) {
)
}

addConfigPatch := func(configPatches []string, configOpt func(jsonpatch.Patch) bundle.Option) error {
var jsonPatch jsonpatch.Patch
addConfigPatch := func(configPatches []string, configOpt func([]configpatcher.Patch) bundle.Option) error {
var patches []configpatcher.Patch

jsonPatch, err = configpatcher.LoadPatches(configPatches)
patches, err = configpatcher.LoadPatches(configPatches)
if err != nil {
return fmt.Errorf("error parsing config JSON patch: %w", err)
}

configBundleOpts = append(configBundleOpts, configOpt(jsonPatch))
configBundleOpts = append(configBundleOpts, configOpt(patches))

return nil
}

if err = addConfigPatch(configPatch, bundle.WithJSONPatch); err != nil {
if err = addConfigPatch(configPatch, bundle.WithPatch); err != nil {
return err
}

if err = addConfigPatch(configPatchControlPlane, bundle.WithJSONPatchControlPlane); err != nil {
if err = addConfigPatch(configPatchControlPlane, bundle.WithPatchControlPlane); err != nil {
return err
}

if err = addConfigPatch(configPatchWorker, bundle.WithJSONPatchWorker); err != nil {
if err = addConfigPatch(configPatchWorker, bundle.WithPatchWorker); err != nil {
return err
}

Expand Down
15 changes: 7 additions & 8 deletions cmd/talosctl/cmd/mgmt/gen/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"path/filepath"
"strings"

jsonpatch "github.com/evanphx/json-patch"
"github.com/spf13/cobra"
talosnet "github.com/talos-systems/net"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -100,7 +99,7 @@ func V1Alpha1Config(genOptions []generate.GenOption,
configPatch []string,
configPatchControlPlane []string,
configPatchWorker []string,
) (*v1alpha1.ConfigBundle, error) {
) (*bundle.ConfigBundle, error) {
configBundleOpts := []bundle.Option{
bundle.WithInputOptions(
&bundle.InputOptions{
Expand All @@ -112,26 +111,26 @@ func V1Alpha1Config(genOptions []generate.GenOption,
),
}

addConfigPatch := func(configPatches []string, configOpt func(jsonpatch.Patch) bundle.Option) error {
jsonPatch, err := configpatcher.LoadPatches(configPatches)
addConfigPatch := func(configPatches []string, configOpt func([]configpatcher.Patch) bundle.Option) error {
patches, err := configpatcher.LoadPatches(configPatches)
if err != nil {
return fmt.Errorf("error parsing config JSON patch: %w", err)
}

configBundleOpts = append(configBundleOpts, configOpt(jsonPatch))
configBundleOpts = append(configBundleOpts, configOpt(patches))

return nil
}

if err := addConfigPatch(configPatch, bundle.WithJSONPatch); err != nil {
if err := addConfigPatch(configPatch, bundle.WithPatch); err != nil {
return nil, err
}

if err := addConfigPatch(configPatchControlPlane, bundle.WithJSONPatchControlPlane); err != nil {
if err := addConfigPatch(configPatchControlPlane, bundle.WithPatchControlPlane); err != nil {
return nil, err
}

if err := addConfigPatch(configPatchWorker, bundle.WithJSONPatchWorker); err != nil {
if err := addConfigPatch(configPatchWorker, bundle.WithPatchWorker); err != nil {
return nil, err
}

Expand Down
14 changes: 9 additions & 5 deletions cmd/talosctl/cmd/talos/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"time"

jsonpatch "github.com/evanphx/json-patch"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/durationpb"
yaml "gopkg.in/yaml.v3"
Expand All @@ -34,7 +33,7 @@ var patchCmdFlags struct {
configTryTimeout time.Duration
}

func patchFn(c *client.Client, patch jsonpatch.Patch) func(context.Context, client.ResourceResponse) error {
func patchFn(c *client.Client, patches []configpatcher.Patch) func(context.Context, client.ResourceResponse) error {
return func(ctx context.Context, msg client.ResourceResponse) error {
if msg.Resource == nil {
if msg.Definition.Metadata().ID() != strings.ToLower(config.MachineConfigType) {
Expand All @@ -49,7 +48,12 @@ func patchFn(c *client.Client, patch jsonpatch.Patch) func(context.Context, clie
return err
}

patched, err := configpatcher.JSON6902(body, patch)
cfg, err := configpatcher.Apply(configpatcher.WithBytes(body), patches)
if err != nil {
return err
}

patched, err := cfg.Bytes()
if err != nil {
return err
}
Expand Down Expand Up @@ -99,14 +103,14 @@ var patchCmd = &cobra.Command{
return fmt.Errorf("either --patch or --patch-file should be defined")
}

patch, err := configpatcher.LoadPatches(patchCmdFlags.patch)
patches, err := configpatcher.LoadPatches(patchCmdFlags.patch)
if err != nil {
return err
}

for _, node := range Nodes {
nodeCtx := client.WithNodes(ctx, node)
if err := helpers.ForEachResource(nodeCtx, c, patchFn(c, patch), patchCmdFlags.namespace, args...); err != nil {
if err := helpers.ForEachResource(nodeCtx, c, patchFn(c, patches), patchCmdFlags.namespace, args...); err != nil {
return err
}
}
Expand Down
15 changes: 15 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ vlan=eth1.5:eth1 ip=172.20.0.2::172.20.0.1:255.255.255.0::eth1.5:::::
```http://example.com/metadata?h=${hostname}&m=${mac}&s=${serial}&u=${uuid}```
"""

[notes.strategic-merge]
title = "Strategic merge machine configuration patching"
description="""\
In addition to JSON (RFC6902) patches Talos now supports [strategic merge patching](https://www.talos.dev/v1.2/talos-guides/configuration/patching/).
For example, machine hostname can be set with the following patch:
```yaml
machine:
network:
hostname: worker1
```
Patch format is detected automatically.
"""

[make_deps]

Expand Down
24 changes: 20 additions & 4 deletions internal/integration/cli/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func (suite *GenSuite) TestGenConfigURLValidation() {
base.StderrShouldMatch(regexp.MustCompile(`\Qtry: "https://192.168.0.1:2000"`)))
}

// TestGenConfigPatch verifies that gen config --config-patch works.
func (suite *GenSuite) TestGenConfigPatch() {
// TestGenConfigPatchJSON6902 verifies that gen config --config-patch works with JSON patches.
func (suite *GenSuite) TestGenConfigPatchJSON6902() {
patch, err := json.Marshal([]map[string]interface{}{
{
"op": "replace",
Expand All @@ -141,6 +141,23 @@ func (suite *GenSuite) TestGenConfigPatch() {

suite.Assert().NoError(err)

suite.testGenConfigPatch(patch)
}

// TestGenConfigPatchStrategic verifies that gen config --config-patch works with strategic merge patches.
func (suite *GenSuite) TestGenConfigPatchStrategic() {
patch, err := yaml.Marshal(map[string]interface{}{
"cluster": map[string]interface{}{
"clusterName": "bar",
},
})

suite.Assert().NoError(err)

suite.testGenConfigPatch(patch)
}

func (suite *GenSuite) testGenConfigPatch(patch []byte) {
for _, tt := range []struct {
flag string
shouldAffect map[string]bool
Expand Down Expand Up @@ -172,8 +189,7 @@ func (suite *GenSuite) TestGenConfigPatch() {

for _, configName := range []string{"controlplane.yaml", "worker.yaml"} {
cfg, err := configloader.NewFromFile(configName)

suite.Assert().NoError(err)
suite.Require().NoError(err)

switch {
case tt.shouldAffect[configName]:
Expand Down
2 changes: 1 addition & 1 deletion internal/integration/provision/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ type UpgradeSuite struct {

provisioner provision.Provisioner

configBundle *v1alpha1.ConfigBundle
configBundle *bundle.ConfigBundle

clusterAccess *access.Adapter
controlPlaneEndpoint string
Expand Down
111 changes: 111 additions & 0 deletions pkg/machinery/config/configpatcher/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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 configpatcher

import (
jsonpatch "github.com/evanphx/json-patch"

"github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/config/configloader"
"github.com/talos-systems/talos/pkg/machinery/config/encoder"
)

// configOrBytes encapsulates either unmarshaled config or raw byte representation.
type configOrBytes struct {
marshaled []byte
config config.Provider
}

func (cb *configOrBytes) Bytes() ([]byte, error) {
if cb.marshaled != nil {
return cb.marshaled, nil
}

var err error

cb.marshaled, err = cb.config.EncodeBytes(encoder.WithComments(encoder.CommentsDisabled))
if err != nil {
return nil, err
}

cb.config = nil

return cb.marshaled, nil
}

func (cb *configOrBytes) Config() (config.Provider, error) {
if cb.config != nil {
return cb.config, nil
}

var err error

cb.config, err = configloader.NewFromBytes(cb.marshaled)
if err != nil {
return nil, err
}

cb.marshaled = nil

return cb.config, nil
}

// Input to the patch application process.
type Input interface {
Config() (config.Provider, error)
Bytes() ([]byte, error)
}

// WithConfig returns a new Input that wraps the given config.
func WithConfig(config config.Provider) Input {
return &configOrBytes{config: config}
}

// WithBytes returns a new Input that wraps the given bytes.
func WithBytes(bytes []byte) Input {
return &configOrBytes{marshaled: bytes}
}

// Output of patch application process.
type Output = Input

// Apply config patches to Talos machine config.
//
// Apply either JSON6902 or StrategicMergePatch.
//
// This method tries to minimize conversion between byte and unmarshalled
// config representation as much as possible.
func Apply(in Input, patches []Patch) (Output, error) {
for _, patch := range patches {
switch p := patch.(type) {
case jsonpatch.Patch:
bytes, err := in.Bytes()
if err != nil {
return nil, err
}

patched, err := JSON6902(bytes, p)
if err != nil {
return nil, err
}

in = WithBytes(patched)
case StrategicMergePatch:
cfg, err := in.Config()
if err != nil {
return nil, err
}

patched, err := StrategicMerge(cfg, p)
if err != nil {
return nil, err
}

in = WithConfig(patched)
}
}

return in, nil
}
59 changes: 59 additions & 0 deletions pkg/machinery/config/configpatcher/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 configpatcher_test

import (
_ "embed"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/talos-systems/talos/pkg/machinery/config/configloader"
"github.com/talos-systems/talos/pkg/machinery/config/configpatcher"
)

//go:embed testdata/apply/config.yaml
var config []byte

//go:embed testdata/apply/expected.yaml
var expected []byte

func TestApply(t *testing.T) {
patches, err := configpatcher.LoadPatches([]string{
"@testdata/apply/strategic1.yaml",
"@testdata/apply/jsonpatch1.yaml",
"@testdata/apply/jsonpatch2.yaml",
"@testdata/apply/strategic2.yaml",
})
require.NoError(t, err)

cfg, err := configloader.NewFromBytes(config)
require.NoError(t, err)

for _, tt := range []struct {
name string
input configpatcher.Input
}{
{
name: "WithConfig",
input: configpatcher.WithConfig(cfg),
},
{
name: "WithBytes",
input: configpatcher.WithBytes(config),
},
} {
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, expected, bytes)
})
}
}
Loading

0 comments on commit 641f6a1

Please sign in to comment.