From 159264f665142f9569abac98391e245e8eeede27 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Wed, 1 May 2024 14:14:39 +0200 Subject: [PATCH] Add YAMLs differ Signed-off-by: Andrei Kvapil --- pkg/commands/template.go | 250 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 239 insertions(+), 11 deletions(-) diff --git a/pkg/commands/template.go b/pkg/commands/template.go index 1f4a270..7834f2d 100644 --- a/pkg/commands/template.go +++ b/pkg/commands/template.go @@ -5,6 +5,7 @@ package commands import ( + "bytes" "context" "encoding/json" "fmt" @@ -12,6 +13,7 @@ import ( "path/filepath" "reflect" "strconv" + "strings" "unsafe" "gopkg.in/yaml.v3" @@ -26,10 +28,11 @@ import ( "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/strvals" - "github.com/siderolabs/talos/cmd/talosctl/cmd/mgmt/gen" "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers" "github.com/siderolabs/talos/pkg/cli" "github.com/siderolabs/talos/pkg/machinery/client" + "github.com/siderolabs/talos/pkg/machinery/config/bundle" + "github.com/siderolabs/talos/pkg/machinery/config/configpatcher" "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" @@ -54,7 +57,7 @@ var templateCmd = &cobra.Command{ Use: "template", Short: "Render chart templates locally and display the output", Long: ``, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if templateCmdFlags.insecure { return WithClientMaintenance(nil, render(args)) @@ -139,10 +142,14 @@ func render(args []string) func(ctx context.Context, c *client.Client) error { return err } - requestedTemplate := filepath.Join(chrt.Name(), args[0]) - configPatch, ok := out[requestedTemplate] - if !ok { - return fmt.Errorf("template %s not found", args[0]) + var configPatches []string + for _, arg := range args { + requestedTemplate := filepath.Join(chrt.Name(), arg) + configPatch, ok := out[requestedTemplate] + if !ok { + return fmt.Errorf("template %s not found", arg) + } + configPatches = append(configPatches, configPatch) } var genOptions []generate.Option //nolint:prealloc @@ -158,19 +165,71 @@ func render(args []string) func(ctx context.Context, c *client.Client) error { genOptions = append(genOptions, generate.WithSecretsBundle(secretsBundle)) } - configBundle, err := gen.GenerateConfigBundle(genOptions, "", "", "", []string{configPatch}, []string{}, []string{}) + if err != nil { + return err + } + configFinal := []byte(configPatches[len(configPatches)-1]) + configBundleOpts := []bundle.Option{ + bundle.WithInputOptions( + &bundle.InputOptions{ + ClusterName: "clusterName", + Endpoint: "endpoint", + KubeVersion: strings.TrimPrefix("kubernetesVersion", "v"), + GenOptions: genOptions, + }, + ), + } + patches, err := configpatcher.LoadPatches(configPatches) + configBundle, err := bundle.NewBundle(configBundleOpts...) + var configOrigin []byte + if !templateCmdFlags.full { + configOrigin, err = configBundle.Serialize(encoder.CommentsDisabled, machine.TypeControlPlane) + if err != nil { + return err + } + } + configBundle.ApplyPatches(patches, true, true) configFull, err := configBundle.Serialize(encoder.CommentsDisabled, machine.TypeControlPlane) if err != nil { return err } + configPatches = append(configPatches, string(configFull)) + // Copy comments + if len(configPatches) > 1 { + + var target []byte + if templateCmdFlags.full { + target = []byte(configPatches[len(configPatches)-1]) + } else { + target, err = diffYAMLs(configOrigin, configFull) + if err != nil { + return nil + } + } + var targetNode yaml.Node + if err := yaml.Unmarshal(target, &targetNode); err != nil { + return err + } - if templateCmdFlags.full { - fmt.Println(string(configFull)) - } else { - fmt.Println(string(configPatch)) + for _, configPatch := range configPatches[:len(configPatches)-1] { + var sourceNode yaml.Node + if err := yaml.Unmarshal([]byte(configPatch), &sourceNode); err != nil { + return err + } + + dstPaths := make(map[string]*yaml.Node) + copyComments(&sourceNode, &targetNode, "", dstPaths) + applyComments(&targetNode, "", dstPaths) + } + configFinal, err = yaml.Marshal(&targetNode) + if err != nil { + return err + } } + fmt.Println(string(configFinal)) + return nil } } @@ -363,3 +422,172 @@ func mergeMaps(a, b map[string]interface{}) map[string]interface{} { } return out } + +// copyComments updates the comments in dstNode considering the structure of whitespace. +func copyComments(srcNode, dstNode *yaml.Node, path string, dstPaths map[string]*yaml.Node) { + // Save the path of the current node in dstPaths if there are comments. + if srcNode.HeadComment != "" || srcNode.LineComment != "" || srcNode.FootComment != "" { + dstPaths[path] = srcNode + } + + // Recursive traversal for child elements. + for i := 0; i < len(srcNode.Content); i++ { + newPath := path + "/" + srcNode.Content[i].Value // For nodes with keys. + if srcNode.Kind == yaml.SequenceNode { + newPath = path + "/" + string(i) // For lists. + } + copyComments(srcNode.Content[i], dstNode, newPath, dstPaths) + } +} + +// applyComments applies the copied comments to the target document. +func applyComments(dstNode *yaml.Node, path string, dstPaths map[string]*yaml.Node) { + if srcNode, ok := dstPaths[path]; ok { + dstNode.HeadComment = mergeComments(dstNode.HeadComment, srcNode.HeadComment) + dstNode.LineComment = mergeComments(dstNode.LineComment, srcNode.LineComment) + dstNode.FootComment = mergeComments(dstNode.FootComment, srcNode.FootComment) + } + + // Apply to child elements. + for i := 0; i < len(dstNode.Content); i++ { + newPath := path + "/" + dstNode.Content[i].Value // For nodes with keys. + if dstNode.Kind == yaml.SequenceNode { + newPath = path + "/" + string(i) // For lists. + } + applyComments(dstNode.Content[i], newPath, dstPaths) + } +} + +// mergeComments combines old and new comments considering empty lines. +func mergeComments(oldComment, newComment string) string { + if oldComment == "" { + return newComment + } + if newComment == "" { + return oldComment + } + return strings.TrimSpace(oldComment) + "\n\n" + strings.TrimSpace(newComment) +} + +// ---------------- +// diffYAMLs compares two YAML documents and outputs the differences, including relevant comments. +// TODO: comments are not compared, and they should not +// TODO: lists should contain only missing items +func diffYAMLs(original, modified []byte) ([]byte, error) { + var origNode, modNode yaml.Node + if err := yaml.Unmarshal(original, &origNode); err != nil { + return nil, err + } + if err := yaml.Unmarshal(modified, &modNode); err != nil { + return nil, err + } + + diff := compareNodes(origNode.Content[0], modNode.Content[0]) + if diff == nil { // If there are no differences + return []byte{}, nil + } + + buffer := &bytes.Buffer{} + encoder := yaml.NewEncoder(buffer) + encoder.SetIndent(2) + if err := encoder.Encode(diff); err != nil { + return nil, err + } + encoder.Close() + + return buffer.Bytes(), nil +} + +// compareNodes recursively finds differences between two YAML nodes. +func compareNodes(orig, mod *yaml.Node) *yaml.Node { + if orig.Kind != mod.Kind { + return mod // Different kinds means definitely changed. + } + + switch orig.Kind { + case yaml.MappingNode: + return compareMappingNodes(orig, mod) + case yaml.SequenceNode: + return compareSequenceNodes(orig, mod) + case yaml.ScalarNode: + if orig.Value != mod.Value { + return mod // Different scalar values mean changed. + } + } + return nil // No differences found +} + +// compareMappingNodes compares two mapping nodes and returns differences. +func compareMappingNodes(orig, mod *yaml.Node) *yaml.Node { + diff := &yaml.Node{Kind: yaml.MappingNode} + origMap := nodeMap(orig) + modMap := nodeMap(mod) + + for k, modVal := range modMap { + origVal, ok := origMap[k] + if !ok { + // Key not in original, it's an addition. + addNodeToDiff(diff, k, modVal) + } else { + changedNode := compareNodes(origVal, modVal) + if changedNode != nil { + addNodeToDiff(diff, k, changedNode) + } + } + } + + if len(diff.Content) == 0 { + return nil // No differences. + } + return diff +} + +// compareSequenceNodes compares two sequence nodes. +func compareSequenceNodes(orig, mod *yaml.Node) *yaml.Node { + // Simple sequence comparison: by index (naive but effective for many use cases). + diff := &yaml.Node{Kind: yaml.SequenceNode} + minLength := min(len(orig.Content), len(mod.Content)) + for i := 0; i < minLength; i++ { + changedNode := compareNodes(orig.Content[i], mod.Content[i]) + if changedNode != nil { + diff.Content = append(diff.Content, changedNode) + } + } + if len(mod.Content) > minLength { // Additional items in mod + diff.Content = append(diff.Content, mod.Content[minLength:]...) + } + + if len(diff.Content) == 0 { + return nil + } + return diff +} + +// Utility functions + +// nodeMap creates a map from a YAML mapping node for easy lookup. +func nodeMap(node *yaml.Node) map[string]*yaml.Node { + result := make(map[string]*yaml.Node) + for i := 0; i+1 < len(node.Content); i += 2 { + keyNode := node.Content[i] + if keyNode.Kind == yaml.ScalarNode { + result[keyNode.Value] = node.Content[i+1] + } + } + return result +} + +// addNodeToDiff adds a node to the diff result. +func addNodeToDiff(diff *yaml.Node, key string, node *yaml.Node) { + keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: key} + diff.Content = append(diff.Content, keyNode) + diff.Content = append(diff.Content, node) +} + +// min returns the smaller of x or y. +func min(x, y int) int { + if x < y { + return x + } + return y +}