chore: handle documents diff in apply-config dry run

Before this PR diff generator only diffed the v1alpha1 config and nothing else. With this PR it also takes
separate docs into the account.

```shell
~ > <editor> controlplane.yaml
~ > talosctl -n talos-default-controlplane-1  apply-config --file controlplane.yaml --dry-run
Dry run summary:
Applied configuration without a reboot (skipped in dry-run).
Config diff:
No changes.
Documents diff:
[]config.Document{
+	&runtime.KmsgLogV1Alpha1{
+		Meta:       meta.Meta{MetaAPIVersion: "v1alpha1", MetaKind: "KmsgLogConfig"},
+		MetaName:   "omni-kmsg",
+		KmsgLogURL: s"tcp://[fdae:41e4:649b:9303::1]:8092",
+	},
}
~ > talosctl -n talos-default-controlplane-1  apply-config --file controlplane.yaml
Applied configuration without a reboot
~ >
~ >
~ >
~ > <editor> controlplane.yaml
~ > talosctl -n talos-default-controlplane-1  apply-config --file controlplane.yaml --dry-run
Dry run summary:
Applied configuration without a reboot (skipped in dry-run).
Config diff:
No changes.
Documents diff:
[]config.Document{
	&runtime.KmsgLogV1Alpha1{Meta: {MetaAPIVersion: "v1alpha1", MetaKind: "KmsgLogConfig"}, MetaName: "omni-kmsg", KmsgLogURL: {URL: &{Scheme: "tcp", Host: "[fdae:41e4:649b:9303::1]:8092"}}},
+	&network.DefaultActionConfigV1Alpha1{
+		Meta:    meta.Meta{MetaAPIVersion: "v1alpha1", MetaKind: "NetworkDefaultActionConfig"},
+		Ingress: s"block",
+	},
}
```

Closes #8885

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
This commit is contained in:
Dmitriy Matrenichev 2024-06-27 20:25:33 +03:00
parent bd34f71f3e
commit 2d054ad355
No known key found for this signature in database
GPG Key ID: 94B473337258BFD5
3 changed files with 111 additions and 47 deletions

View File

@ -43,7 +43,7 @@ var applyConfigCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
var ( var (
cfgBytes []byte cfgBytes []byte
e error err error
) )
if len(args) > 0 { if len(args) > 0 {
@ -59,9 +59,9 @@ var applyConfigCmd = &cobra.Command{
} }
if applyConfigCmdFlags.filename != "" { if applyConfigCmdFlags.filename != "" {
cfgBytes, e = os.ReadFile(applyConfigCmdFlags.filename) cfgBytes, err = os.ReadFile(applyConfigCmdFlags.filename)
if e != nil { if err != nil {
return fmt.Errorf("failed to read configuration from %q: %w", applyConfigCmdFlags.filename, e) return fmt.Errorf("failed to read configuration from %q: %w", applyConfigCmdFlags.filename, err)
} }
if len(cfgBytes) < 1 { if len(cfgBytes) < 1 {
@ -74,19 +74,19 @@ var applyConfigCmd = &cobra.Command{
patches []configpatcher.Patch patches []configpatcher.Patch
) )
patches, e = configpatcher.LoadPatches(applyConfigCmdFlags.patches) patches, err = configpatcher.LoadPatches(applyConfigCmdFlags.patches)
if e != nil { if err != nil {
return e return err
} }
cfg, e = configpatcher.Apply(configpatcher.WithBytes(cfgBytes), patches) cfg, err = configpatcher.Apply(configpatcher.WithBytes(cfgBytes), patches)
if e != nil { if err != nil {
return e return err
} }
cfgBytes, e = cfg.Bytes() cfgBytes, err = cfg.Bytes()
if e != nil { if err != nil {
return e return err
} }
} }
} else if applyConfigCmdFlags.Mode.Mode != helpers.InteractiveMode { } else if applyConfigCmdFlags.Mode.Mode != helpers.InteractiveMode {
@ -108,8 +108,10 @@ var applyConfigCmd = &cobra.Command{
if len(GlobalArgs.Endpoints) > 0 { if len(GlobalArgs.Endpoints) > 0 {
return WithClientNoNodes(func(bootstrapCtx context.Context, bootstrapClient *client.Client) error { return WithClientNoNodes(func(bootstrapCtx context.Context, bootstrapClient *client.Client) error {
opts := []installer.Option{} opts := []installer.Option{
opts = append(opts, installer.WithBootstrapNode(bootstrapCtx, bootstrapClient, GlobalArgs.Endpoints[0]), installer.WithDryRun(applyConfigCmdFlags.dryRun)) installer.WithBootstrapNode(bootstrapCtx, bootstrapClient, GlobalArgs.Endpoints[0]),
installer.WithDryRun(applyConfigCmdFlags.dryRun),
}
conn, err := installer.NewConnection( conn, err := installer.NewConnection(
ctx, ctx,

View File

@ -8,6 +8,7 @@ import (
"archive/tar" "archive/tar"
"bufio" "bufio"
"bytes" "bytes"
stdcmp "cmp"
"compress/gzip" "compress/gzip"
"context" "context"
"encoding/json" "encoding/json"
@ -76,6 +77,8 @@ import (
"github.com/siderolabs/talos/pkg/machinery/api/storage" "github.com/siderolabs/talos/pkg/machinery/api/storage"
timeapi "github.com/siderolabs/talos/pkg/machinery/api/time" timeapi "github.com/siderolabs/talos/pkg/machinery/api/time"
clientconfig "github.com/siderolabs/talos/pkg/machinery/client/config" clientconfig "github.com/siderolabs/talos/pkg/machinery/client/config"
"github.com/siderolabs/talos/pkg/machinery/config"
docscfg "github.com/siderolabs/talos/pkg/machinery/config/config"
"github.com/siderolabs/talos/pkg/machinery/config/configloader" "github.com/siderolabs/talos/pkg/machinery/config/configloader"
"github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets"
machinetype "github.com/siderolabs/talos/pkg/machinery/config/machine" machinetype "github.com/siderolabs/talos/pkg/machinery/config/machine"
@ -218,15 +221,7 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig
} }
if in.DryRun { if in.DryRun {
var config interface{} details := generateDiff(s.Controller.Runtime(), cfgProvider)
if s.Controller.Runtime().Config() != nil {
config = s.Controller.Runtime().ConfigContainer().RawV1Alpha1()
}
diff := cmp.Diff(config, cfgProvider.RawV1Alpha1(), cmp.AllowUnexported(v1alpha1.InstallDiskSizeMatcher{}))
if diff == "" {
diff = "No changes."
}
return &machine.ApplyConfigurationResponse{ return &machine.ApplyConfigurationResponse{
Messages: []*machine.ApplyConfiguration{ Messages: []*machine.ApplyConfiguration{
@ -234,8 +229,7 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig
Mode: in.Mode, Mode: in.Mode,
ModeDetails: fmt.Sprintf(`Dry run summary: ModeDetails: fmt.Sprintf(`Dry run summary:
%s (skipped in dry-run). %s (skipped in dry-run).
Config diff: %s`, modeDetails, details),
%s`, modeDetails, diff),
}, },
}, },
}, nil }, nil
@ -301,6 +295,35 @@ Config diff:
}, nil }, nil
} }
func generateDiff(r runtime.Runtime, provider config.Provider) string {
var cfg *v1alpha1.Config
if r.Config() != nil {
cfg = r.ConfigContainer().RawV1Alpha1()
}
v1alpha1Diff := cmp.Diff(cfg, provider.RawV1Alpha1(), cmp.AllowUnexported(v1alpha1.InstallDiskSizeMatcher{}))
if v1alpha1Diff == "" {
v1alpha1Diff = "No changes."
}
origDocs := slices.DeleteFunc(r.ConfigContainer().Documents(), func(doc docscfg.Document) bool { return doc.Kind() == v1alpha1.Version })
newDocs := slices.DeleteFunc(provider.Documents(), func(doc docscfg.Document) bool { return doc.Kind() == v1alpha1.Version })
slices.SortStableFunc(origDocs, func(a, b docscfg.Document) int { return stdcmp.Compare(a.Kind(), b.Kind()) })
slices.SortStableFunc(newDocs, func(a, b docscfg.Document) int { return stdcmp.Compare(a.Kind(), b.Kind()) })
documentsDiff := cmp.Diff(origDocs, newDocs)
if documentsDiff == "" {
documentsDiff = "No changes."
}
return fmt.Sprintf(`Config diff:
%s
Documents diff:
%s`, v1alpha1Diff, documentsDiff)
}
// GenerateConfiguration implements the machine.MachineServer interface. // GenerateConfiguration implements the machine.MachineServer interface.
func (s *Server) GenerateConfiguration(ctx context.Context, in *machine.GenerateConfigurationRequest) (reply *machine.GenerateConfigurationResponse, err error) { func (s *Server) GenerateConfiguration(ctx context.Context, in *machine.GenerateConfigurationRequest) (reply *machine.GenerateConfigurationResponse, err error) {
if s.Controller.Runtime().Config().Machine().Type() == machinetype.TypeWorker { if s.Controller.Runtime().Config().Machine().Type() == machinetype.TypeWorker {

View File

@ -8,12 +8,14 @@ package api
import ( import (
"context" "context"
"net/url"
"os" "os"
"sort" "slices"
"testing" "testing"
"time" "time"
"github.com/cosi-project/runtime/pkg/safe" "github.com/cosi-project/runtime/pkg/safe"
"github.com/siderolabs/gen/ensure"
"github.com/siderolabs/go-pointer" "github.com/siderolabs/go-pointer"
"github.com/siderolabs/go-retry/retry" "github.com/siderolabs/go-retry/retry"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@ -23,7 +25,9 @@ import (
machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine" machineapi "github.com/siderolabs/talos/pkg/machinery/api/machine"
"github.com/siderolabs/talos/pkg/machinery/client" "github.com/siderolabs/talos/pkg/machinery/client"
"github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config"
"github.com/siderolabs/talos/pkg/machinery/config/container"
"github.com/siderolabs/talos/pkg/machinery/config/machine" "github.com/siderolabs/talos/pkg/machinery/config/machine"
"github.com/siderolabs/talos/pkg/machinery/config/types/runtime"
"github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1"
"github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/constants"
mc "github.com/siderolabs/talos/pkg/machinery/resources/config" mc "github.com/siderolabs/talos/pkg/machinery/resources/config"
@ -88,14 +92,13 @@ func (suite *ApplyConfigSuite) TestApply() {
suite.WaitForBootDone(suite.ctx) suite.WaitForBootDone(suite.ctx)
sort.Strings(nodes) slices.Sort(nodes)
node := nodes[0] node := nodes[0]
nodeCtx := client.WithNode(suite.ctx, node) nodeCtx := client.WithNode(suite.ctx, node)
provider, err := suite.ReadConfigFromNode(nodeCtx) provider, err := suite.ReadConfigFromNode(nodeCtx)
suite.Assert().Nilf(err, "failed to read existing config from node %q: %w", node, err) suite.Assert().NoErrorf(err, "failed to read existing config from node %q: %w", node, err)
cfgDataOut := suite.PatchV1Alpha1Config(provider, func(cfg *v1alpha1.Config) { cfgDataOut := suite.PatchV1Alpha1Config(provider, func(cfg *v1alpha1.Config) {
if cfg.MachineConfig.MachineSysctls == nil { if cfg.MachineConfig.MachineSysctls == nil {
@ -115,7 +118,7 @@ func (suite *ApplyConfigSuite) TestApply() {
) )
if err != nil { if err != nil {
// It is expected that the connection will EOF here, so just log the error // It is expected that the connection will EOF here, so just log the error
suite.Assert().Nilf(err, "failed to apply configuration (node %q): %w", node, err) suite.Assert().NoErrorf(err, "failed to apply configuration (node %q): %w", node, err)
} }
return nil return nil
@ -125,7 +128,7 @@ func (suite *ApplyConfigSuite) TestApply() {
// Verify configuration change // Verify configuration change
var newProvider config.Provider var newProvider config.Provider
suite.Require().Nilf( suite.Require().NoErrorf(
retry.Constant(time.Minute, retry.WithUnits(time.Second)).Retry( retry.Constant(time.Minute, retry.WithUnits(time.Second)).Retry(
func() error { func() error {
newProvider, err = suite.ReadConfigFromNode(nodeCtx) newProvider, err = suite.ReadConfigFromNode(nodeCtx)
@ -300,7 +303,7 @@ func (suite *ApplyConfigSuite) TestApplyConfigRotateEncryptionSecrets() {
) )
if err != nil { if err != nil {
// It is expected that the connection will EOF here, so just log the error // It is expected that the connection will EOF here, so just log the error
suite.Assert().Nilf(err, "failed to apply configuration (node %q): %w", node, err) suite.Assert().Errorf(err, "failed to apply configuration (node %q): %w", node, err)
} }
return nil return nil
@ -312,7 +315,7 @@ func (suite *ApplyConfigSuite) TestApplyConfigRotateEncryptionSecrets() {
// Verify configuration change // Verify configuration change
var newProvider config.Provider var newProvider config.Provider
suite.Require().Nilf( suite.Require().Errorf(
retry.Constant(time.Minute, retry.WithUnits(time.Second)).Retry( retry.Constant(time.Minute, retry.WithUnits(time.Second)).Retry(
func() error { func() error {
newProvider, err = suite.ReadConfigFromNode(nodeCtx) newProvider, err = suite.ReadConfigFromNode(nodeCtx)
@ -355,14 +358,13 @@ func (suite *ApplyConfigSuite) TestApplyNoReboot() {
suite.WaitForBootDone(suite.ctx) suite.WaitForBootDone(suite.ctx)
sort.Strings(nodes) slices.Sort(nodes)
node := nodes[0] node := nodes[0]
nodeCtx := client.WithNode(suite.ctx, node) nodeCtx := client.WithNode(suite.ctx, node)
provider, err := suite.ReadConfigFromNode(nodeCtx) provider, err := suite.ReadConfigFromNode(nodeCtx)
suite.Require().Nilf(err, "failed to read existing config from node %q: %s", node, err) suite.Require().NoErrorf(err, "failed to read existing config from node %q: %s", node, err)
cfgDataOut := suite.PatchV1Alpha1Config(provider, func(cfg *v1alpha1.Config) { cfgDataOut := suite.PatchV1Alpha1Config(provider, func(cfg *v1alpha1.Config) {
// this won't be possible without a reboot // this won't be possible without a reboot
@ -387,14 +389,13 @@ func (suite *ApplyConfigSuite) TestApplyDryRun() {
suite.WaitForBootDone(suite.ctx) suite.WaitForBootDone(suite.ctx)
sort.Strings(nodes) slices.Sort(nodes)
node := nodes[0] node := nodes[0]
nodeCtx := client.WithNode(suite.ctx, node) nodeCtx := client.WithNode(suite.ctx, node)
provider, err := suite.ReadConfigFromNode(nodeCtx) provider, err := suite.ReadConfigFromNode(nodeCtx)
suite.Require().Nilf(err, "failed to read existing config from node %q: %s", node, err) suite.Require().NoErrorf(err, "failed to read existing config from node %q: %s", node, err)
cfgDataOut := suite.PatchV1Alpha1Config(provider, func(cfg *v1alpha1.Config) { cfgDataOut := suite.PatchV1Alpha1Config(provider, func(cfg *v1alpha1.Config) {
// this won't be possible without a reboot // this won't be possible without a reboot
@ -416,10 +417,49 @@ func (suite *ApplyConfigSuite) TestApplyDryRun() {
}, },
) )
suite.Require().Nilf(err, "failed to apply configuration (node %q): %s", node, err) suite.Require().NoErrorf(err, "failed to apply configuration (node %q): %s", node, err)
suite.Assert().Contains(reply.Messages[0].ModeDetails, "Dry run summary") suite.Assert().Contains(reply.Messages[0].ModeDetails, "Dry run summary")
} }
// TestApplyDryRunDocuments verifies the apply config API with multi doc and dry run enabled.
func (suite *ApplyConfigSuite) TestApplyDryRunDocuments() {
nodes := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeWorker)
suite.Require().NotEmpty(nodes)
suite.WaitForBootDone(suite.ctx)
slices.Sort(nodes)
node := nodes[0]
nodeCtx := client.WithNode(suite.ctx, node)
provider, err := suite.ReadConfigFromNode(nodeCtx)
suite.Require().NoErrorf(err, "failed to read existing config from node %q: %s", node, err)
kmsg := runtime.NewKmsgLogV1Alpha1()
kmsg.MetaName = "omni-kmsg"
kmsg.KmsgLogURL.URL = ensure.Value(url.Parse("tcp://[fdae:41e4:649b:9303::1]:8092"))
cont, err := container.New(provider.RawV1Alpha1(), kmsg)
suite.Require().NoErrorf(err, "failed to create container: %s", err)
cfgDataOut, err := cont.Bytes()
suite.Require().NoErrorf(err, "failed to marshal container: %s", err)
reply, err := suite.Client.ApplyConfiguration(
nodeCtx, &machineapi.ApplyConfigurationRequest{
Data: cfgDataOut,
Mode: machineapi.ApplyConfigurationRequest_AUTO,
DryRun: true,
},
)
suite.Require().NoErrorf(err, "failed to apply configuration (node %q): %s", node, err)
suite.Assert().Contains(reply.Messages[0].ModeDetails, "Dry run summary")
suite.Assert().Contains(reply.Messages[0].ModeDetails, "omni-kmsg")
suite.Assert().Contains(reply.Messages[0].ModeDetails, "tcp://[fdae:41e4:649b:9303::1]:8092")
}
// TestApplyTry applies the config in try mode with a short timeout. // TestApplyTry applies the config in try mode with a short timeout.
func (suite *ApplyConfigSuite) TestApplyTry() { func (suite *ApplyConfigSuite) TestApplyTry() {
nodes := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeWorker) nodes := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeWorker)
@ -427,10 +467,9 @@ func (suite *ApplyConfigSuite) TestApplyTry() {
suite.WaitForBootDone(suite.ctx) suite.WaitForBootDone(suite.ctx)
sort.Strings(nodes) slices.Sort(nodes)
node := nodes[0] node := nodes[0]
nodeCtx := client.WithNode(suite.ctx, node) nodeCtx := client.WithNode(suite.ctx, node)
getMachineConfig := func(ctx context.Context) (*mc.MachineConfig, error) { getMachineConfig := func(ctx context.Context) (*mc.MachineConfig, error) {
@ -443,7 +482,7 @@ func (suite *ApplyConfigSuite) TestApplyTry() {
} }
provider, err := getMachineConfig(nodeCtx) provider, err := getMachineConfig(nodeCtx)
suite.Require().Nilf(err, "failed to read existing config from node %q: %s", node, err) suite.Require().NoErrorf(err, "failed to read existing config from node %q: %s", node, err)
cfgDataOut := suite.PatchV1Alpha1Config(provider.Provider(), func(cfg *v1alpha1.Config) { cfgDataOut := suite.PatchV1Alpha1Config(provider.Provider(), func(cfg *v1alpha1.Config) {
if cfg.MachineConfig.MachineNetwork == nil { if cfg.MachineConfig.MachineNetwork == nil {
@ -465,10 +504,10 @@ func (suite *ApplyConfigSuite) TestApplyTry() {
TryModeTimeout: durationpb.New(time.Second * 1), TryModeTimeout: durationpb.New(time.Second * 1),
}, },
) )
suite.Assert().Nilf(err, "failed to apply configuration (node %q): %s", node, err) suite.Assert().NoErrorf(err, "failed to apply configuration (node %q): %s", node, err)
provider, err = getMachineConfig(nodeCtx) provider, err = getMachineConfig(nodeCtx)
suite.Require().Nilf(err, "failed to read existing config from node %q: %w", node, err) suite.Require().NoErrorf(err, "failed to read existing config from node %q: %w", node, err)
suite.Assert().NotNil(provider.Config().Machine().Network()) suite.Assert().NotNil(provider.Config().Machine().Network())
suite.Assert().NotNil(provider.Config().Machine().Network().Devices()) suite.Assert().NotNil(provider.Config().Machine().Network().Devices())
@ -487,7 +526,7 @@ func (suite *ApplyConfigSuite) TestApplyTry() {
for range 100 { for range 100 {
provider, err = getMachineConfig(nodeCtx) provider, err = getMachineConfig(nodeCtx)
suite.Assert().Nilf(err, "failed to read existing config from node %q: %s", node, err) suite.Assert().NoErrorf(err, "failed to read existing config from node %q: %s", node, err)
if provider.Config().Machine().Network() == nil { if provider.Config().Machine().Network() == nil {
return return