Utku Ozdemir a89d270cd3
fix: replace gotextdiff with linear-space Myers diff to prevent OOM
The gotextdiff/myers library uses the naive Myers algorithm variant that stores the full edit trace, resulting in O((M+N)^2) space complexity.

For machine configs with large inline K8s manifests (thousands of lines), this causes massive memory spikes — e.g., 80K lines allocates ~98 GB and gets OOM-killed.

Replace it with neticdk/go-stdlib/diff/myers which implements the linear-space Myers variant (divide-and-conquer). Memory usage drops from ~25 GB to ~8 MB for 40K-line inputs.

The diff output format is unchanged (unified diff with @@ hunks).

Co-authored-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
Co-authored-by: Oguz Kilcan <oguz.kilcan@siderolabs.com>
Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
2026-02-12 15:06:43 +01:00

118 lines
2.6 KiB
Go

// 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 utils contains various utility functions for template operations.
package utils
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/fatih/color"
"go.yaml.in/yaml/v4"
"github.com/siderolabs/omni/client/pkg/diff"
)
// MarshalResource to YAML format (bytes).
func MarshalResource(r resource.Resource) ([]byte, error) {
var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
m, err := resource.MarshalYAML(r)
if err != nil {
return nil, err
}
if err := enc.Encode(m); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// RenderDiff outputs colorized diff between two resources.
//
// One of the resources might be nil.
func RenderDiff(w io.Writer, oldR, newR resource.Resource) error {
var (
oldYaml, newYaml []byte
oldPath, newPath string
err error
)
if oldR != nil {
oldYaml, err = MarshalResource(oldR)
if err != nil {
return err
}
oldPath = resource.String(oldR)
} else {
oldPath = "/dev/null"
}
if newR != nil {
newYaml, err = MarshalResource(newR)
if err != nil {
return err
}
newPath = resource.String(newR)
} else {
newPath = "/dev/null"
}
diffStr, err := diff.Compute(oldYaml, newYaml)
if err != nil {
return err
}
outputDiff(w, diffStr, oldPath, newPath)
return nil
}
func outputDiff(w io.Writer, diffStr, fromPath, toPath string) {
// Strip the library's generic header; we print our own with resource paths.
diffStr, _ = strings.CutPrefix(diffStr, "--- a\n+++ b\n")
if diffStr == "" {
return
}
bold := color.New(color.Bold)
bold.Fprintf(w, "--- %s\n", fromPath) //nolint:errcheck
bold.Fprintf(w, "+++ %s\n", toPath) //nolint:errcheck
cyan := color.New(color.FgCyan)
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
for line := range strings.SplitSeq(diffStr, "\n") {
switch {
case strings.HasPrefix(line, "@@"):
cyan.Fprintln(w, line) //nolint:errcheck
case strings.HasPrefix(line, "-"):
red.Fprintln(w, line) //nolint:errcheck
case strings.HasPrefix(line, "+"):
green.Fprintln(w, line) //nolint:errcheck
case line == "":
// skip trailing empty line
default:
fmt.Fprintln(w, line) //nolint:errcheck
}
}
}
// Describe a resources in human readable format.
func Describe(r resource.Resource) string {
return fmt.Sprintf("%s(%s)", r.Metadata().Type(), r.Metadata().ID())
}