@@ -18,15 +18,19 @@ package cmd
18
18
19
19
import (
20
20
"fmt"
21
+ "io"
21
22
"os"
22
23
"path"
24
+ "path/filepath"
25
+ "regexp"
23
26
"sort"
27
+ "strings"
24
28
25
- "github.com/google/go-cmp/cmp"
26
29
"github.com/olekukonko/tablewriter"
27
30
"github.com/pkg/errors"
28
31
"github.com/spf13/cobra"
29
32
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33
+ "k8s.io/utils/exec"
30
34
crclient "sigs.k8s.io/controller-runtime/pkg/client"
31
35
32
36
"sigs.k8s.io/cluster-api/cmd/clusterctl/client"
@@ -276,10 +280,13 @@ func writeOutputFiles(out *cluster.TopologyPlanOutput, outDir string) error {
276
280
}
277
281
278
282
// Calculate the diff and write to a file.
279
- diff := cmp .Diff (m .Before , m .After )
280
283
diffFileName := fmt .Sprintf ("%s_%s_%s.diff" , m .After .GetKind (), m .After .GetNamespace (), m .After .GetName ())
281
284
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 {
283
290
return errors .Wrapf (err , "failed to write diff to file %q" , diffFilePath )
284
291
}
285
292
}
@@ -329,3 +336,51 @@ func addRow(table *tablewriter.Table, o *unstructured.Unstructured, action strin
329
336
},
330
337
)
331
338
}
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