Skip to content

Commit

Permalink
Add YAMLs differ
Browse files Browse the repository at this point in the history
Signed-off-by: Andrei Kvapil <[email protected]>
  • Loading branch information
kvaps committed May 2, 2024
1 parent 8f33ea1 commit 159264f
Showing 1 changed file with 239 additions and 11 deletions.
250 changes: 239 additions & 11 deletions pkg/commands/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
package commands

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"unsafe"

"gopkg.in/yaml.v3"
Expand All @@ -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"
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
}

0 comments on commit 159264f

Please sign in to comment.