package diff
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"text/template"
"github.com/aryann/difflib"
"github.com/gonvenience/ytbx"
"github.com/homeport/dyff/pkg/dyff"
"github.com/mgutz/ansi"
)
type Report struct {
format ReportFormat
Entries []ReportEntry
mode string
findRenames float32
}
type ReportEntry struct {
Key string
SuppressedKinds []string
Kind string
Context int
Diffs []difflib.DiffRecord
ChangeType string
Structured *StructuredEntry
}
type ReportFormat struct {
output func(r *Report, to io.Writer)
changestyles map[string]ChangeStyle
}
type ChangeStyle struct {
color string
message string
}
type ReportTemplateSpec struct {
Namespace string
Name string
Kind string
API string
Change string
}
func (r *Report) setupReportFormat(format string) {
r.mode = format
switch format {
case "simple":
setupSimpleReport(r)
case "template":
setupTemplateReport(r)
case "json":
setupJSONReport(r)
case "structured":
setupStructuredReport(r)
case "dyff":
setupDyffReport(r)
default:
setupDiffReport(r)
}
}
func setupDyffReport(r *Report) {
r.format.output = printDyffReport
}
func printDyffReport(r *Report, to io.Writer) {
currentFile, _ := os.CreateTemp("", "existing-values")
defer func() {
_ = os.Remove(currentFile.Name())
}()
newFile, _ := os.CreateTemp("", "new-values")
defer func() {
_ = os.Remove(newFile.Name())
}()
for _, entry := range r.Entries {
_, _ = currentFile.WriteString("---\n")
_, _ = newFile.WriteString("---\n")
for _, record := range entry.Diffs {
switch record.Delta {
case difflib.Common:
_, _ = currentFile.WriteString(record.Payload + "\n")
_, _ = newFile.WriteString(record.Payload + "\n")
case difflib.LeftOnly:
_, _ = currentFile.WriteString(record.Payload + "\n")
case difflib.RightOnly:
_, _ = newFile.WriteString(record.Payload + "\n")
}
}
}
_ = currentFile.Close()
_ = newFile.Close()
currentInputFile, newInputFile, _ := ytbx.LoadFiles(currentFile.Name(), newFile.Name())
var compareOptions []dyff.CompareOption
compareOptions = append(compareOptions,
dyff.IgnoreWhitespaceChanges(true),
dyff.KubernetesEntityDetection(true),
)
if r.findRenames > 0 {
compareOptions = append(compareOptions, dyff.DetectRenames(true))
}
report, _ := dyff.CompareInputFiles(currentInputFile, newInputFile, compareOptions...)
reportWriter := &dyff.HumanReport{
Report: report,
OmitHeader: true,
MinorChangeThreshold: 0.1,
}
_ = reportWriter.WriteReport(to)
}
func (r *Report) addEntry(key string, suppressedKinds []string, kind string, context int, diffs []difflib.DiffRecord, changeType string, structured *StructuredEntry) {
entry := ReportEntry{
key,
suppressedKinds,
kind,
context,
diffs,
changeType,
structured,
}
r.Entries = append(r.Entries, entry)
}
func (r *Report) print(to io.Writer) {
r.format.output(r, to)
}
func (r *Report) clean() {
r.Entries = nil
}
func setupDiffReport(r *Report) {
r.format.output = printDiffReport
r.format.changestyles = make(map[string]ChangeStyle)
r.format.changestyles["ADD"] = ChangeStyle{color: "green", message: "has been added:"}
r.format.changestyles["REMOVE"] = ChangeStyle{color: "red", message: "has been removed:"}
r.format.changestyles["MODIFY"] = ChangeStyle{color: "yellow", message: "has changed:"}
r.format.changestyles["OWNERSHIP"] = ChangeStyle{color: "magenta", message: "changed ownership:"}
r.format.changestyles["MODIFY_SUPPRESSED"] = ChangeStyle{color: "blue+h", message: "has changed, but diff is empty after suppression."}
}
func printDiffReport(r *Report, to io.Writer) {
for _, entry := range r.Entries {
_, _ = fmt.Fprintf(
to,
ansi.Color("%s %s", r.format.changestyles[entry.ChangeType].color)+"\n",
entry.Key,
r.format.changestyles[entry.ChangeType].message,
)
printDiffRecords(entry.SuppressedKinds, entry.Kind, entry.Context, entry.Diffs, to)
}
}
func setupSimpleReport(r *Report) {
r.format.output = printSimpleReport
r.format.changestyles = make(map[string]ChangeStyle)
r.format.changestyles["ADD"] = ChangeStyle{color: "green", message: "to be added."}
r.format.changestyles["REMOVE"] = ChangeStyle{color: "red", message: "to be removed."}
r.format.changestyles["MODIFY"] = ChangeStyle{color: "yellow", message: "to be changed."}
r.format.changestyles["OWNERSHIP"] = ChangeStyle{color: "magenta", message: "to change ownership."}
r.format.changestyles["MODIFY_SUPPRESSED"] = ChangeStyle{color: "blue+h", message: "has changed, but diff is empty after suppression."}
}
func printSimpleReport(r *Report, to io.Writer) {
summary := map[string]int{
"ADD": 0,
"REMOVE": 0,
"MODIFY": 0,
"OWNERSHIP": 0,
"MODIFY_SUPPRESSED": 0,
}
for _, entry := range r.Entries {
_, _ = fmt.Fprintf(to, ansi.Color("%s %s", r.format.changestyles[entry.ChangeType].color)+"\n",
entry.Key,
r.format.changestyles[entry.ChangeType].message,
)
summary[entry.ChangeType]++
}
_, _ = fmt.Fprintf(to, "Plan: %d to add, %d to change, %d to destroy, %d to change ownership.\n", summary["ADD"], summary["MODIFY"], summary["REMOVE"], summary["OWNERSHIP"])
}
func newTemplate(name string) *template.Template {
funcsMap := template.FuncMap{
"last": func(x int, a interface{}) bool {
return x == reflect.ValueOf(a).Len()-1
},
}
return template.New(name).Funcs(funcsMap)
}
func setupJSONReport(r *Report) {
t, err := newTemplate("entries").Parse(defaultTemplateReport)
if err != nil {
log.Fatalf("Error loading default template: %v", err)
}
r.format.output = templateReportPrinter(t)
r.format.changestyles = make(map[string]ChangeStyle)
r.format.changestyles["ADD"] = ChangeStyle{color: "green", message: ""}
r.format.changestyles["REMOVE"] = ChangeStyle{color: "red", message: ""}
r.format.changestyles["MODIFY"] = ChangeStyle{color: "yellow", message: ""}
r.format.changestyles["OWNERSHIP"] = ChangeStyle{color: "magenta", message: ""}
r.format.changestyles["MODIFY_SUPPRESSED"] = ChangeStyle{color: "blue+h", message: ""}
}
func setupTemplateReport(r *Report) {
var tpl *template.Template
{
tplFile, present := os.LookupEnv("HELM_DIFF_TPL")
if present {
t, err := newTemplate(filepath.Base(tplFile)).ParseFiles(tplFile)
if err != nil {
fmt.Println(err)
log.Fatalf("Error loading custom template")
}
tpl = t
} else {
t, err := newTemplate("entries").Parse(defaultTemplateReport)
if err != nil {
log.Fatalf("Error loading default template")
}
tpl = t
}
}
r.format.output = templateReportPrinter(tpl)
r.format.changestyles = make(map[string]ChangeStyle)
r.format.changestyles["ADD"] = ChangeStyle{color: "green", message: ""}
r.format.changestyles["REMOVE"] = ChangeStyle{color: "red", message: ""}
r.format.changestyles["MODIFY"] = ChangeStyle{color: "yellow", message: ""}
r.format.changestyles["OWNERSHIP"] = ChangeStyle{color: "magenta", message: ""}
r.format.changestyles["MODIFY_SUPPRESSED"] = ChangeStyle{color: "blue+h", message: ""}
}
func setupStructuredReport(r *Report) {
r.format.output = printStructuredReport
}
func printStructuredReport(r *Report, to io.Writer) {
entries := make([]StructuredEntry, 0, len(r.Entries))
for _, entry := range r.Entries {
if entry.Structured != nil {
structuredCopy := *entry.Structured
if structuredCopy.ChangeType == "" {
structuredCopy.ChangeType = entry.ChangeType
}
entries = append(entries, structuredCopy)
continue
}
entries = append(entries, StructuredEntry{
Name: entry.Key,
ChangeType: entry.ChangeType,
})
}
encoder := json.NewEncoder(to)
encoder.SetIndent("", " ")
if err := encoder.Encode(entries); err != nil {
log.Printf("Error encoding structured diff output: %v\n", err)
}
}
func (t *ReportTemplateSpec) loadFromKey(key string) error {
pattern := regexp.MustCompile(`(?P<namespace>[a-z0-9-]+), (?P<name>[a-z0-9.-]+), (?P<kind>\w+) \((?P<api>[^)]+)\)`)
matches := pattern.FindStringSubmatch(key)
if len(matches) > 1 {
t.Namespace = matches[1]
t.Name = matches[2]
t.Kind = matches[3]
t.API = matches[4]
return nil
}
return errors.New("key string didn't match regexp")
}
func templateReportPrinter(t *template.Template) func(r *Report, to io.Writer) {
return func(r *Report, to io.Writer) {
var templateDataArray []ReportTemplateSpec
for _, entry := range r.Entries {
templateData := ReportTemplateSpec{}
err := templateData.loadFromKey(entry.Key)
if err != nil {
log.Println("error processing report entry")
} else {
templateData.Change = entry.ChangeType
templateDataArray = append(templateDataArray, templateData)
}
}
_ = t.Execute(to, templateDataArray)
_, _ = to.Write([]byte("\n"))
}
}