diff --git a/cmd/options.go b/cmd/options.go new file mode 100644 index 00000000..e78d2517 --- /dev/null +++ b/cmd/options.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/databus23/helm-diff/v3/diff" + "github.com/spf13/pflag" +) + +// AddDiffOptions adds flags for the various consolidated options to the functions in the diff package +func AddDiffOptions(f *pflag.FlagSet, o *diff.Options) { + f.BoolP("suppress-secrets", "q", false, "suppress secrets in the output") + f.BoolVar(&o.ShowSecrets, "show-secrets", false, "do not redact secret values in the output") + f.StringArrayVar(&o.SuppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") + f.IntVarP(&o.OutputContext, "context", "C", -1, "output NUM lines of context around changes") + f.StringVar(&o.OutputFormat, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.") + f.BoolVar(&o.StripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input") + f.Float32VarP(&o.FindRenames, "find-renames", "D", 0, "Enable rename detection if set to any value greater than 0. If specified, the value denotes the maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a rename. Only objects of the same Kind are attempted to be matched") +} + +// ProcessDiffOptions processes the set flags and handles possible interactions between them +func ProcessDiffOptions(f *pflag.FlagSet, o *diff.Options) { + if q, _ := f.GetBool("suppress-secrets"); q { + o.SuppressedKinds = append(o.SuppressedKinds, "Secret") + } +} diff --git a/cmd/release.go b/cmd/release.go index 1a4a6492..0cff4119 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -15,14 +15,10 @@ import ( type release struct { client helm.Interface detailedExitCode bool - suppressedKinds []string releases []string - outputContext int includeTests bool - showSecrets bool - output string - stripTrailingCR bool normalizeManifests bool + diff.Options } const releaseCmdLongUsage = ` @@ -59,9 +55,7 @@ func releaseCmd() *cobra.Command { return errors.New("Too few arguments to Command \"release\".\nMinimum 2 arguments required: release name-1, release name-2") } - if q, _ := cmd.Flags().GetBool("suppress-secrets"); q { - diff.suppressedKinds = append(diff.suppressedKinds, "Secret") - } + ProcessDiffOptions(cmd.Flags(), &diff.Options) diff.releases = args[0:] if isHelm3() { @@ -74,15 +68,10 @@ func releaseCmd() *cobra.Command { }, } - releaseCmd.Flags().BoolP("suppress-secrets", "q", false, "suppress secrets in the output") - releaseCmd.Flags().BoolVar(&diff.showSecrets, "show-secrets", false, "do not redact secret values in the output") releaseCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") - releaseCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") - releaseCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") releaseCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") - releaseCmd.Flags().StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.") - releaseCmd.Flags().BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input") releaseCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + AddDiffOptions(releaseCmd.Flags(), &diff.Options) releaseCmd.SuggestionsMinimumDistance = 1 @@ -121,11 +110,7 @@ func (d *release) differentiateHelm3() error { seenAnyChanges := diff.Releases( manifest.Parse(string(releaseResponse1), namespace, d.normalizeManifests, excludes...), manifest.Parse(string(releaseResponse2), namespace, d.normalizeManifests, excludes...), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { @@ -156,11 +141,7 @@ func (d *release) differentiate() error { seenAnyChanges := diff.Releases( manifest.ParseRelease(releaseResponse1.Release, d.includeTests, d.normalizeManifests), manifest.ParseRelease(releaseResponse2.Release, d.includeTests, d.normalizeManifests), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { diff --git a/cmd/revision.go b/cmd/revision.go index 59b29b7d..10dfe776 100644 --- a/cmd/revision.go +++ b/cmd/revision.go @@ -17,14 +17,10 @@ type revision struct { release string client helm.Interface detailedExitCode bool - suppressedKinds []string revisions []string - outputContext int includeTests bool - showSecrets bool - output string - stripTrailingCR bool normalizeManifests bool + diff.Options } const revisionCmdLongUsage = ` @@ -68,9 +64,7 @@ func revisionCmd() *cobra.Command { return errors.New("Too many arguments to Command \"revision\".\nMaximum 3 arguments allowed: release name, revision1, revision2") } - if q, _ := cmd.Flags().GetBool("suppress-secrets"); q { - diff.suppressedKinds = append(diff.suppressedKinds, "Secret") - } + ProcessDiffOptions(cmd.Flags(), &diff.Options) diff.release = args[0] diff.revisions = args[1:] @@ -84,15 +78,10 @@ func revisionCmd() *cobra.Command { }, } - revisionCmd.Flags().BoolP("suppress-secrets", "q", false, "suppress secrets in the output") - revisionCmd.Flags().BoolVar(&diff.showSecrets, "show-secrets", false, "do not redact secret values in the output") revisionCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") - revisionCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") - revisionCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") revisionCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") - revisionCmd.Flags().StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.") - revisionCmd.Flags().BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input") revisionCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + AddDiffOptions(revisionCmd.Flags(), &diff.Options) revisionCmd.SuggestionsMinimumDistance = 1 @@ -126,11 +115,7 @@ func (d *revision) differentiateHelm3() error { diff.Manifests( manifest.Parse(string(revisionResponse), namespace, d.normalizeManifests, excludes...), manifest.Parse(string(releaseResponse), namespace, d.normalizeManifests, excludes...), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) case 2: @@ -153,11 +138,7 @@ func (d *revision) differentiateHelm3() error { seenAnyChanges := diff.Manifests( manifest.Parse(string(revisionResponse1), namespace, d.normalizeManifests, excludes...), manifest.Parse(string(revisionResponse2), namespace, d.normalizeManifests, excludes...), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { @@ -193,11 +174,7 @@ func (d *revision) differentiate() error { diff.Manifests( manifest.ParseRelease(revisionResponse.Release, d.includeTests, d.normalizeManifests), manifest.ParseRelease(releaseResponse.Release, d.includeTests, d.normalizeManifests), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) case 2: @@ -220,11 +197,7 @@ func (d *revision) differentiate() error { seenAnyChanges := diff.Manifests( manifest.ParseRelease(revisionResponse1.Release, d.includeTests, d.normalizeManifests), manifest.ParseRelease(revisionResponse2.Release, d.includeTests, d.normalizeManifests), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { diff --git a/cmd/rollback.go b/cmd/rollback.go index 9d46050c..b187cbe0 100644 --- a/cmd/rollback.go +++ b/cmd/rollback.go @@ -17,14 +17,10 @@ type rollback struct { release string client helm.Interface detailedExitCode bool - suppressedKinds []string revisions []string - outputContext int includeTests bool - showSecrets bool - output string - stripTrailingCR bool normalizeManifests bool + diff.Options } const rollbackCmdLongUsage = ` @@ -57,9 +53,7 @@ func rollbackCmd() *cobra.Command { return err } - if q, _ := cmd.Flags().GetBool("suppress-secrets"); q { - diff.suppressedKinds = append(diff.suppressedKinds, "Secret") - } + ProcessDiffOptions(cmd.Flags(), &diff.Options) diff.release = args[0] diff.revisions = args[1:] @@ -76,15 +70,10 @@ func rollbackCmd() *cobra.Command { }, } - rollbackCmd.Flags().BoolP("suppress-secrets", "q", false, "suppress secrets in the output") - rollbackCmd.Flags().BoolVar(&diff.showSecrets, "show-secrets", false, "do not redact secret values in the output") rollbackCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") - rollbackCmd.Flags().StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") - rollbackCmd.Flags().IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") rollbackCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") - rollbackCmd.Flags().StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.") - rollbackCmd.Flags().BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input") rollbackCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + AddDiffOptions(rollbackCmd.Flags(), &diff.Options) rollbackCmd.SuggestionsMinimumDistance = 1 @@ -119,11 +108,7 @@ func (d *rollback) backcastHelm3() error { seenAnyChanges := diff.Manifests( manifest.Parse(string(releaseResponse), namespace, d.normalizeManifests, excludes...), manifest.Parse(string(revisionResponse), namespace, d.normalizeManifests, excludes...), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { @@ -156,11 +141,7 @@ func (d *rollback) backcast() error { seenAnyChanges := diff.Manifests( manifest.ParseRelease(releaseResponse.Release, d.includeTests, d.normalizeManifests), manifest.ParseRelease(revisionResponse.Release, d.includeTests, d.normalizeManifests), - d.suppressedKinds, - d.showSecrets, - d.outputContext, - d.output, - d.stripTrailingCR, + &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { diff --git a/cmd/upgrade.go b/cmd/upgrade.go index ebb6aba1..d6b91b72 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -51,17 +51,13 @@ type diffCmd struct { allowUnreleased bool noHooks bool includeTests bool - suppressedKinds []string - outputContext int - showSecrets bool postRenderer string - output string install bool - stripTrailingCR bool normalizeManifests bool threeWayMerge bool extraAPIs []string useUpgradeDryRun bool + diff.Options } func (d *diffCmd) isAllowUnreleased() bool { @@ -132,9 +128,7 @@ func newChartCommand() *cobra.Command { } } - if q, _ := cmd.Flags().GetBool("suppress-secrets"); q { - diff.suppressedKinds = append(diff.suppressedKinds, "Secret") - } + ProcessDiffOptions(cmd.Flags(), &diff.Options) diff.release = args[0] diff.chart = args[1] @@ -163,8 +157,6 @@ func newChartCommand() *cobra.Command { // - https://github.com/helm/helm/blob/d9ffe37d371c9d06448c55c852c800051830e49a/cmd/helm/template.go#L184 // - https://github.com/databus23/helm-diff/issues/318 f.StringArrayVarP(&diff.extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") - f.BoolP("suppress-secrets", "q", false, "suppress secrets in the output") - f.BoolVar(&diff.showSecrets, "show-secrets", false, "do not redact secret values in the output") f.VarP(&diff.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)") f.StringArrayVar(&diff.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&diff.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") @@ -176,15 +168,14 @@ func newChartCommand() *cobra.Command { f.BoolVar(&diff.noHooks, "no-hooks", false, "disable diffing of hooks") f.BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") f.BoolVar(&diff.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.StringArrayVar(&diff.suppressedKinds, "suppress", []string{}, "allows suppression of the values listed in the diff output") - f.IntVarP(&diff.outputContext, "context", "C", -1, "output NUM lines of context around changes") f.BoolVar(&diff.disableValidation, "disable-validation", false, "disables rendered templates validation against the Kubernetes cluster you are currently pointing to. This is the same validation performed on an install") f.BoolVar(&diff.disableOpenAPIValidation, "disable-openapi-validation", false, "disables rendered templates validation against the Kubernetes OpenAPI Schema") f.BoolVar(&diff.dryRun, "dry-run", false, "disables cluster access and show diff as if it was install. Implies --install, --reset-values, and --disable-validation") f.StringVar(&diff.postRenderer, "post-renderer", "", "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path") - f.StringVar(&diff.output, "output", "diff", "Possible values: diff, simple, json, template. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.") - f.BoolVar(&diff.stripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input") f.BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + + AddDiffOptions(f, &diff.Options) + if !isHelm3() { f.StringVar(&diff.namespace, "namespace", "default", "namespace to assume the release to be installed into") } @@ -271,7 +262,7 @@ func (d *diffCmd) runHelm3() error { } else { newSpecs = manifest.Parse(string(installManifest), d.namespace, d.normalizeManifests, helm3TestHook, helm2TestSuccessHook) } - seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.showSecrets, d.outputContext, d.output, d.stripTrailingCR, os.Stdout) + seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { return Error{ @@ -463,7 +454,7 @@ func (d *diffCmd) run() error { } } - seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.showSecrets, d.outputContext, d.output, d.stripTrailingCR, os.Stdout) + seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, &d.Options, os.Stdout) if d.detailedExitCode && seenAnyChanges { return Error{ diff --git a/diff/diff.go b/diff/diff.go index c4634c84..9ed30bbb 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -18,62 +18,139 @@ import ( "github.com/databus23/helm-diff/v3/manifest" ) +// Options are all the options to be passed to generate a diff +type Options struct { + OutputFormat string + OutputContext int + StripTrailingCR bool + ShowSecrets bool + SuppressedKinds []string + FindRenames float32 +} + // Manifests diff on manifests -func Manifests(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, showSecrets bool, context int, output string, stripTrailingCR bool, to io.Writer) bool { +func Manifests(oldIndex, newIndex map[string]*manifest.MappingResult, options *Options, to io.Writer) bool { report := Report{} - report.setupReportFormat(output) - seenAnyChanges := false - emptyMapping := &manifest.MappingResult{} + report.setupReportFormat(options.OutputFormat) + var possiblyRemoved []string + for _, key := range sortedKeys(oldIndex) { oldContent := oldIndex[key] if newContent, ok := newIndex[key]; ok { - if oldContent.Content != newContent.Content { - // modified - if !showSecrets { - redactSecrets(oldContent, newContent) - } - - diffs := diffMappingResults(oldContent, newContent, stripTrailingCR) - if len(diffs) > 0 { - seenAnyChanges = true - } - report.addEntry(key, suppressedKinds, oldContent.Kind, context, diffs, "MODIFY") - } + // modified? + doDiff(&report, key, oldContent, newContent, options) } else { - // removed - if !showSecrets { - redactSecrets(oldContent, nil) - - } - diffs := diffMappingResults(oldContent, emptyMapping, stripTrailingCR) - if len(diffs) > 0 { - seenAnyChanges = true - } - report.addEntry(key, suppressedKinds, oldContent.Kind, context, diffs, "REMOVE") + possiblyRemoved = append(possiblyRemoved, key) } } + var possiblyAdded []string for _, key := range sortedKeys(newIndex) { - newContent := newIndex[key] - if _, ok := oldIndex[key]; !ok { - // added - if !showSecrets { - redactSecrets(nil, newContent) - } - diffs := diffMappingResults(emptyMapping, newContent, stripTrailingCR) - if len(diffs) > 0 { - seenAnyChanges = true - } - report.addEntry(key, suppressedKinds, newContent.Kind, context, diffs, "ADD") + possiblyAdded = append(possiblyAdded, key) } } + + removed, added := contentSearch(&report, possiblyRemoved, oldIndex, possiblyAdded, newIndex, options) + + for _, key := range removed { + oldContent := oldIndex[key] + doDiff(&report, key, oldContent, nil, options) + } + + for _, key := range added { + newContent := newIndex[key] + doDiff(&report, key, nil, newContent, options) + } + + seenAnyChanges := len(report.entries) > 0 report.print(to) report.clean() return seenAnyChanges } +func actualChanges(diff []difflib.DiffRecord) int { + changes := 0 + for _, record := range diff { + if record.Delta != difflib.Common { + changes++ + } + } + return changes +} + +func contentSearch(report *Report, possiblyRemoved []string, oldIndex map[string]*manifest.MappingResult, possiblyAdded []string, newIndex map[string]*manifest.MappingResult, options *Options) ([]string, []string) { + if options.FindRenames <= 0 { + return possiblyRemoved, possiblyAdded + } + + var removed []string + + for _, removedKey := range possiblyRemoved { + oldContent := oldIndex[removedKey] + var smallestKey string + var smallestFraction float32 = math.MaxFloat32 + for _, addedKey := range possiblyAdded { + newContent := newIndex[addedKey] + if oldContent.Kind != newContent.Kind { + continue + } + + if !options.ShowSecrets { + redactSecrets(oldContent, newContent) + } + + diff := diffMappingResults(oldContent, newContent, options.StripTrailingCR) + delta := actualChanges(diff) + if delta == 0 || len(diff) == 0 { + continue // Should never happen, but better safe than sorry + } + fraction := float32(delta) / float32(len(diff)) + if fraction > 0 && fraction < smallestFraction { + smallestKey = addedKey + smallestFraction = fraction + } + } + + if smallestFraction < options.FindRenames { + index := sort.SearchStrings(possiblyAdded, smallestKey) + possiblyAdded = append(possiblyAdded[:index], possiblyAdded[index+1:]...) + newContent := newIndex[smallestKey] + doDiff(report, removedKey, oldContent, newContent, options) + } else { + removed = append(removed, removedKey) + } + } + + return removed, possiblyAdded +} + +func doDiff(report *Report, key string, oldContent *manifest.MappingResult, newContent *manifest.MappingResult, options *Options) { + if oldContent != nil && newContent != nil && oldContent.Content == newContent.Content { + return + } + + if !options.ShowSecrets { + redactSecrets(oldContent, newContent) + } + + if oldContent == nil { + emptyMapping := &manifest.MappingResult{} + diffs := diffMappingResults(emptyMapping, newContent, options.StripTrailingCR) + report.addEntry(key, options.SuppressedKinds, newContent.Kind, options.OutputContext, diffs, "ADD") + } else if newContent == nil { + emptyMapping := &manifest.MappingResult{} + diffs := diffMappingResults(oldContent, emptyMapping, options.StripTrailingCR) + report.addEntry(key, options.SuppressedKinds, oldContent.Kind, options.OutputContext, diffs, "REMOVE") + } else { + diffs := diffMappingResults(oldContent, newContent, options.StripTrailingCR) + if actualChanges(diffs) > 0 { + report.addEntry(key, options.SuppressedKinds, oldContent.Kind, options.OutputContext, diffs, "MODIFY") + } + } +} + func redactSecrets(old, new *manifest.MappingResult) { if (old != nil && old.Kind != "Secret") || (new != nil && new.Kind != "Secret") { return @@ -117,7 +194,7 @@ func redactSecrets(old, new *manifest.MappingResult) { if old != nil { oldSecret.Data = nil if err := serializer.Encode(&oldSecret, &buf); err != nil { - + new.Content = fmt.Sprintf("Error encoding new secret: %s", err) } old.Content = getComment(old.Content) + strings.Replace(strings.Replace(buf.String(), "stringData", "data", 1), " creationTimestamp: null\n", "", 1) buf.Reset() //reuse buffer for new secret @@ -125,7 +202,7 @@ func redactSecrets(old, new *manifest.MappingResult) { if new != nil { newSecret.Data = nil if err := serializer.Encode(&newSecret, &buf); err != nil { - + new.Content = fmt.Sprintf("Error encoding new secret: %s", err) } new.Content = getComment(new.Content) + strings.Replace(strings.Replace(buf.String(), "stringData", "data", 1), " creationTimestamp: null\n", "", 1) } @@ -143,10 +220,10 @@ func getComment(s string) string { } // Releases reindex the content based on the template names and pass it to Manifests -func Releases(oldIndex, newIndex map[string]*manifest.MappingResult, suppressedKinds []string, showSecrets bool, context int, output string, stripTrailingCR bool, to io.Writer) bool { +func Releases(oldIndex, newIndex map[string]*manifest.MappingResult, options *Options, to io.Writer) bool { oldIndex = reIndexForRelease(oldIndex) newIndex = reIndexForRelease(newIndex) - return Manifests(oldIndex, newIndex, suppressedKinds, showSecrets, context, output, stripTrailingCR, to) + return Manifests(oldIndex, newIndex, options, to) } func diffMappingResults(oldContent *manifest.MappingResult, newContent *manifest.MappingResult, stripTrailingCR bool) []difflib.DiffRecord { @@ -210,7 +287,11 @@ func printDiffRecord(diff difflib.DiffRecord, to io.Writer) { case difflib.LeftOnly: fmt.Fprintf(to, "%s\n", ansi.Color("- "+text, "red")) case difflib.Common: - fmt.Fprintf(to, "%s\n", " "+text) + if text == "" { + fmt.Fprintln(to) + } else { + fmt.Fprintf(to, "%s\n", " "+text) + } } } diff --git a/diff/diff_test.go b/diff/diff_test.go index 0cab7cc8..667e4a4d 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -195,29 +195,184 @@ metadata: `, }} - t.Run("OnChange", func(t *testing.T) { + specReleaseSpec := map[string]*manifest.MappingResult{ + "default, nginx, Deployment (apps)": { + + Name: "default, nginx, Deployment (apps)", + Kind: "Deployment", + Content: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx +spec: + replicas: 3 +`, + }} + + specReleaseRenamed := map[string]*manifest.MappingResult{ + "default, nginx-renamed, Deployment (apps)": { + Name: "default, nginx-renamed, Deployment (apps)", + Kind: "Deployment", + Content: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-renamed +spec: + replicas: 3 +`, + }} + + specReleaseRenamedAndUpdated := map[string]*manifest.MappingResult{ + "default, nginx-renamed, Deployment (apps)": { + + Name: "default, nginx-renamed, Deployment (apps)", + Kind: "Deployment", + Content: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-renamed +spec: + replicas: 1 +`, + }} + + specReleaseRenamedAndAdded := map[string]*manifest.MappingResult{ + "default, nginx-renamed, Deployment (apps)": { + + Name: "default, nginx-renamed, Deployment (apps)", + Kind: "Deployment", + Content: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-renamed +spec: + replicas: 3 + selector: + matchLabels: + app: nginx-renamed +`, + }} + + t.Run("OnChange", func(t *testing.T) { var buf1 bytes.Buffer + diffOptions := Options{"diff", 10, false, true, []string{}, 0.0} - if changesSeen := Manifests(specBeta, specRelease, []string{}, true, 10, "diff", false, &buf1); !changesSeen { + if changesSeen := Manifests(specBeta, specRelease, &diffOptions, &buf1); !changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") } require.Equal(t, `default, nginx, Deployment (apps) has changed: - + - apiVersion: apps/v1beta1 + apiVersion: apps/v1 kind: Deployment metadata: name: nginx - + +`, buf1.String()) + }) + + t.Run("OnChangeRename", func(t *testing.T) { + var buf1 bytes.Buffer + diffOptions := Options{"diff", 10, false, true, []string{}, 0.5} + + if changesSeen := Manifests(specReleaseSpec, specReleaseRenamed, &diffOptions, &buf1); !changesSeen { + t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") + } + + require.Equal(t, `default, nginx, Deployment (apps) has changed: + + apiVersion: apps/v1 + kind: Deployment + metadata: +- name: nginx ++ name: nginx-renamed + spec: + replicas: 3 + +`, buf1.String()) + }) + + t.Run("OnChangeRenameAndUpdate", func(t *testing.T) { + var buf1 bytes.Buffer + diffOptions := Options{"diff", 10, false, true, []string{}, 0.5} + + if changesSeen := Manifests(specReleaseSpec, specReleaseRenamedAndUpdated, &diffOptions, &buf1); !changesSeen { + t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") + } + + require.Equal(t, `default, nginx, Deployment (apps) has changed: + + apiVersion: apps/v1 + kind: Deployment + metadata: +- name: nginx ++ name: nginx-renamed + spec: +- replicas: 3 ++ replicas: 1 + +`, buf1.String()) + }) + + t.Run("OnChangeRenameAndAdded", func(t *testing.T) { + var buf1 bytes.Buffer + diffOptions := Options{"diff", 10, false, true, []string{}, 0.5} + + if changesSeen := Manifests(specReleaseSpec, specReleaseRenamedAndAdded, &diffOptions, &buf1); !changesSeen { + t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") + } + + require.Equal(t, `default, nginx, Deployment (apps) has changed: + + apiVersion: apps/v1 + kind: Deployment + metadata: +- name: nginx ++ name: nginx-renamed + spec: + replicas: 3 ++ selector: ++ matchLabels: ++ app: nginx-renamed + +`, buf1.String()) + }) + + t.Run("OnChangeRenameAndRemoved", func(t *testing.T) { + var buf1 bytes.Buffer + diffOptions := Options{"diff", 10, false, true, []string{}, 0.5} + + if changesSeen := Manifests(specReleaseRenamedAndAdded, specReleaseSpec, &diffOptions, &buf1); !changesSeen { + t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") + } + + require.Equal(t, `default, nginx-renamed, Deployment (apps) has changed: + + apiVersion: apps/v1 + kind: Deployment + metadata: +- name: nginx-renamed ++ name: nginx + spec: + replicas: 3 +- selector: +- matchLabels: +- app: nginx-renamed + `, buf1.String()) }) t.Run("OnNoChange", func(t *testing.T) { var buf2 bytes.Buffer + diffOptions := Options{"diff", 10, false, true, []string{}, 0.0} - if changesSeen := Manifests(specRelease, specRelease, []string{}, true, 10, "diff", false, &buf2); changesSeen { + if changesSeen := Manifests(specRelease, specRelease, &diffOptions, &buf2); changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `false` to indicate that it has NOT seen any change(s), but was `true`") } @@ -225,10 +380,10 @@ metadata: }) t.Run("OnChangeSimple", func(t *testing.T) { - var buf1 bytes.Buffer + diffOptions := Options{"simple", 10, false, true, []string{}, 0.0} - if changesSeen := Manifests(specBeta, specRelease, []string{}, true, 10, "simple", false, &buf1); !changesSeen { + if changesSeen := Manifests(specBeta, specRelease, &diffOptions, &buf1); !changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") } @@ -239,8 +394,8 @@ Plan: 0 to add, 1 to change, 0 to destroy. t.Run("OnNoChangeSimple", func(t *testing.T) { var buf2 bytes.Buffer - - if changesSeen := Manifests(specRelease, specRelease, []string{}, true, 10, "simple", false, &buf2); changesSeen { + diffOptions := Options{"simple", 10, false, true, []string{}, 0.0} + if changesSeen := Manifests(specRelease, specRelease, &diffOptions, &buf2); changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `false` to indicate that it has NOT seen any change(s), but was `true`") } @@ -248,10 +403,10 @@ Plan: 0 to add, 1 to change, 0 to destroy. }) t.Run("OnChangeTemplate", func(t *testing.T) { - var buf1 bytes.Buffer + diffOptions := Options{"template", 10, false, true, []string{}, 0.0} - if changesSeen := Manifests(specBeta, specRelease, []string{}, true, 10, "template", false, &buf1); !changesSeen { + if changesSeen := Manifests(specBeta, specRelease, &diffOptions, &buf1); !changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") } @@ -266,10 +421,10 @@ Plan: 0 to add, 1 to change, 0 to destroy. }) t.Run("OnChangeJSON", func(t *testing.T) { - var buf1 bytes.Buffer + diffOptions := Options{"json", 10, false, true, []string{}, 0.0} - if changesSeen := Manifests(specBeta, specRelease, []string{}, true, 10, "json", false, &buf1); !changesSeen { + if changesSeen := Manifests(specBeta, specRelease, &diffOptions, &buf1); !changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `true` to indicate that it has seen any change(s), but was `false`") } @@ -285,8 +440,9 @@ Plan: 0 to add, 1 to change, 0 to destroy. t.Run("OnNoChangeTemplate", func(t *testing.T) { var buf2 bytes.Buffer + diffOptions := Options{"template", 10, false, true, []string{}, 0.0} - if changesSeen := Manifests(specRelease, specRelease, []string{}, true, 10, "template", false, &buf2); changesSeen { + if changesSeen := Manifests(specRelease, specRelease, &diffOptions, &buf2); changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `false` to indicate that it has NOT seen any change(s), but was `true`") } @@ -296,7 +452,9 @@ Plan: 0 to add, 1 to change, 0 to destroy. t.Run("OnChangeCustomTemplate", func(t *testing.T) { var buf1 bytes.Buffer os.Setenv("HELM_DIFF_TPL", "testdata/customTemplate.tpl") - if changesSeen := Manifests(specBeta, specRelease, []string{}, true, 10, "template", false, &buf1); !changesSeen { + diffOptions := Options{"template", 10, false, true, []string{}, 0.0} + + if changesSeen := Manifests(specBeta, specRelease, &diffOptions, &buf1); !changesSeen { t.Error("Unexpected return value from Manifests: Expected the return value to be `false` to indicate that it has NOT seen any change(s), but was `true`") }