@@ -18,15 +18,19 @@ package cmd
1818
1919import (
2020 "fmt"
21+ "io"
2122 "os"
2223 "path"
24+ "path/filepath"
25+ "regexp"
2326 "sort"
27+ "strings"
2428
25- "github.com/google/go-cmp/cmp"
2629 "github.com/olekukonko/tablewriter"
2730 "github.com/pkg/errors"
2831 "github.com/spf13/cobra"
2932 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33+ "k8s.io/utils/exec"
3034 crclient "sigs.k8s.io/controller-runtime/pkg/client"
3135
3236 "sigs.k8s.io/cluster-api/cmd/clusterctl/client"
@@ -276,10 +280,13 @@ func writeOutputFiles(out *cluster.TopologyPlanOutput, outDir string) error {
276280 }
277281
278282 // Calculate the diff and write to a file.
279- diff := cmp .Diff (m .Before , m .After )
280283 diffFileName := fmt .Sprintf ("%s_%s_%s.diff" , m .After .GetKind (), m .After .GetNamespace (), m .After .GetName ())
281284 diffFilePath := path .Join (modifiedDir , diffFileName )
282- if err := os .WriteFile (diffFilePath , []byte (diff ), 0600 ); err != nil {
285+ diffFile , err := os .OpenFile (filepath .Clean (diffFilePath ), os .O_WRONLY | os .O_CREATE | os .O_TRUNC , 0600 )
286+ if err != nil {
287+ return errors .Wrapf (err , "unable to open file %q" , diffFilePath )
288+ }
289+ if err := writeDiffToFile (filePathOriginal , filePathModified , diffFile ); err != nil {
283290 return errors .Wrapf (err , "failed to write diff to file %q" , diffFilePath )
284291 }
285292 }
@@ -329,3 +336,51 @@ func addRow(table *tablewriter.Table, o *unstructured.Unstructured, action strin
329336 },
330337 )
331338}
339+
340+ // writeDiffToFile runs the detected diff program. `from` and `to` are the files to diff.
341+ // The implementation is highly inspired by kubectl's DiffProgram implementation:
342+ // ref: https://github.com/kubernetes/kubectl/blob/v0.24.3/pkg/cmd/diff/diff.go#L218
343+ func writeDiffToFile (from , to string , out io.Writer ) error {
344+ diff , cmd := getDiffCommand (from , to )
345+ cmd .SetStdout (out )
346+
347+ if err := cmd .Run (); err != nil && ! isDiffError (err ) {
348+ return errors .Wrapf (err , "failed to run %q" , diff )
349+ }
350+ return nil
351+ }
352+
353+ func getDiffCommand (args ... string ) (string , exec.Cmd ) {
354+ diff := ""
355+ if envDiff := os .Getenv ("KUBECTL_EXTERNAL_DIFF" ); envDiff != "" {
356+ diffCommand := strings .Split (envDiff , " " )
357+ diff = diffCommand [0 ]
358+
359+ if len (diffCommand ) > 1 {
360+ // Regex accepts: Alphanumeric (case-insensitive), dash and equal
361+ isValidChar := regexp .MustCompile (`^[a-zA-Z0-9-=]+$` ).MatchString
362+ for i := 1 ; i < len (diffCommand ); i ++ {
363+ if isValidChar (diffCommand [i ]) {
364+ args = append (args , diffCommand [i ])
365+ }
366+ }
367+ }
368+ } else {
369+ diff = "diff"
370+ args = append ([]string {"-u" , "-N" }, args ... )
371+ }
372+
373+ cmd := exec .New ().Command (diff , args ... )
374+
375+ return diff , cmd
376+ }
377+
378+ // diffError returns true if the status code is lower or equal to 1, false otherwise.
379+ // This makes use of the exit code of diff programs which is 0 for no diff, 1 for
380+ // modified and 2 for other errors.
381+ func isDiffError (err error ) bool {
382+ if err , ok := err .(exec.ExitError ); ok && err .ExitStatus () <= 1 {
383+ return true
384+ }
385+ return false
386+ }
0 commit comments