1
16
17 package diff
18
19 import (
20 "fmt"
21 "io"
22 "os"
23 "path/filepath"
24 "regexp"
25 "strings"
26
27 "github.com/jonboulle/clockwork"
28 "github.com/spf13/cobra"
29 "k8s.io/apimachinery/pkg/api/errors"
30 "k8s.io/apimachinery/pkg/api/meta"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33 "k8s.io/apimachinery/pkg/runtime"
34 "k8s.io/apimachinery/pkg/types"
35 "k8s.io/cli-runtime/pkg/genericiooptions"
36 "k8s.io/cli-runtime/pkg/resource"
37 "k8s.io/client-go/dynamic"
38 "k8s.io/client-go/openapi3"
39 "k8s.io/klog/v2"
40 "k8s.io/kubectl/pkg/cmd/apply"
41 cmdutil "k8s.io/kubectl/pkg/cmd/util"
42 "k8s.io/kubectl/pkg/scheme"
43 "k8s.io/kubectl/pkg/util"
44 "k8s.io/kubectl/pkg/util/i18n"
45 "k8s.io/kubectl/pkg/util/openapi"
46 "k8s.io/kubectl/pkg/util/prune"
47 "k8s.io/kubectl/pkg/util/templates"
48 "k8s.io/utils/exec"
49 "sigs.k8s.io/yaml"
50 )
51
52 var (
53 diffLong = templates.LongDesc(i18n.T(`
54 Diff configurations specified by file name or stdin between the current online
55 configuration, and the configuration as it would be if applied.
56
57 The output is always YAML.
58
59 KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own
60 diff command. Users can use external commands with params too, example:
61 KUBECTL_EXTERNAL_DIFF="colordiff -N -u"
62
63 By default, the "diff" command available in your path will be
64 run with the "-u" (unified diff) and "-N" (treat absent files as empty) options.
65
66 Exit status:
67 0
68 No differences were found.
69 1
70 Differences were found.
71 >1
72 Kubectl or diff failed with an error.
73
74 Note: KUBECTL_EXTERNAL_DIFF, if used, is expected to follow that convention.`))
75
76 diffExample = templates.Examples(i18n.T(`
77 # Diff resources included in pod.json
78 kubectl diff -f pod.json
79
80 # Diff file read from stdin
81 cat service.yaml | kubectl diff -f -`))
82 )
83
84
85 const maxRetries = 4
86
87
88 const (
89 sensitiveMaskDefault = "***"
90 sensitiveMaskBefore = "*** (before)"
91 sensitiveMaskAfter = "*** (after)"
92 )
93
94
95
96 func diffError(err error) exec.ExitError {
97 if err, ok := err.(exec.ExitError); ok && err.ExitStatus() <= 1 {
98 return err
99 }
100 return nil
101 }
102
103 type DiffOptions struct {
104 FilenameOptions resource.FilenameOptions
105
106 ServerSideApply bool
107 FieldManager string
108 ForceConflicts bool
109 ShowManagedFields bool
110
111 Concurrency int
112 Selector string
113 OpenAPIGetter openapi.OpenAPIResourcesGetter
114 OpenAPIV3Root openapi3.Root
115 DynamicClient dynamic.Interface
116 CmdNamespace string
117 EnforceNamespace bool
118 Builder *resource.Builder
119 Diff *DiffProgram
120
121 pruner *pruner
122 tracker *tracker
123 }
124
125 func NewDiffOptions(ioStreams genericiooptions.IOStreams) *DiffOptions {
126 return &DiffOptions{
127 Diff: &DiffProgram{
128 Exec: exec.New(),
129 IOStreams: ioStreams,
130 },
131 }
132 }
133
134 func NewCmdDiff(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
135 options := NewDiffOptions(streams)
136 cmd := &cobra.Command{
137 Use: "diff -f FILENAME",
138 DisableFlagsInUseLine: true,
139 Short: i18n.T("Diff the live version against a would-be applied version"),
140 Long: diffLong,
141 Example: diffExample,
142 Run: func(cmd *cobra.Command, args []string) {
143 cmdutil.CheckDiffErr(options.Complete(f, cmd, args))
144 cmdutil.CheckDiffErr(options.Validate())
145
146
147
148
149
150
151 if err := options.Run(); err != nil {
152 if exitErr := diffError(err); exitErr != nil {
153 cmdutil.CheckErr(cmdutil.ErrExit)
154 }
155 cmdutil.CheckDiffErr(err)
156 }
157 },
158 }
159
160
161
162
163 cmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error {
164 cmdutil.CheckDiffErr(cmdutil.UsageErrorf(cmd, err.Error()))
165 return nil
166 })
167
168 usage := "contains the configuration to diff"
169 cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default allowlist with <group/version/kind> for --prune")
170 cmd.Flags().Bool("prune", false, "Include resources that would be deleted by pruning. Can be used with -l and default shows all resources would be pruned")
171 cmd.Flags().BoolVar(&options.ShowManagedFields, "show-managed-fields", options.ShowManagedFields, "If true, include managed fields in the diff.")
172 cmd.Flags().IntVar(&options.Concurrency, "concurrency", 1, "Number of objects to process in parallel when diffing against the live version. Larger number = faster, but more memory, I/O and CPU over that shorter period of time.")
173 cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
174 cmdutil.AddServerSideApplyFlags(cmd)
175 cmdutil.AddFieldManagerFlagVar(cmd, &options.FieldManager, apply.FieldManagerClientSideApply)
176 cmdutil.AddLabelSelectorFlagVar(cmd, &options.Selector)
177
178 return cmd
179 }
180
181
182
183
184 type DiffProgram struct {
185 Exec exec.Interface
186 genericiooptions.IOStreams
187 }
188
189 func (d *DiffProgram) getCommand(args ...string) (string, exec.Cmd) {
190 diff := ""
191 if envDiff := os.Getenv("KUBECTL_EXTERNAL_DIFF"); envDiff != "" {
192 diffCommand := strings.Split(envDiff, " ")
193 diff = diffCommand[0]
194
195 if len(diffCommand) > 1 {
196
197 isValidChar := regexp.MustCompile(`^[a-zA-Z0-9-=]+$`).MatchString
198 for i := 1; i < len(diffCommand); i++ {
199 if isValidChar(diffCommand[i]) {
200 args = append(args, diffCommand[i])
201 }
202 }
203 }
204 } else {
205 diff = "diff"
206 args = append([]string{"-u", "-N"}, args...)
207 }
208
209 cmd := d.Exec.Command(diff, args...)
210 cmd.SetStdout(d.Out)
211 cmd.SetStderr(d.ErrOut)
212
213 return diff, cmd
214 }
215
216
217 func (d *DiffProgram) Run(from, to string) error {
218 diff, cmd := d.getCommand(from, to)
219 if err := cmd.Run(); err != nil {
220
221
222 if diffErr := diffError(err); diffErr != nil {
223 return diffErr
224 }
225 return fmt.Errorf("failed to run %q: %v", diff, err)
226 }
227 return nil
228 }
229
230
231 type Printer struct{}
232
233
234 func (p *Printer) Print(obj runtime.Object, w io.Writer) error {
235 if obj == nil {
236 return nil
237 }
238 data, err := yaml.Marshal(obj)
239 if err != nil {
240 return err
241 }
242 _, err = w.Write(data)
243 return err
244
245 }
246
247
248 type DiffVersion struct {
249 Dir *Directory
250 Name string
251 }
252
253
254 func NewDiffVersion(name string) (*DiffVersion, error) {
255 dir, err := CreateDirectory(name)
256 if err != nil {
257 return nil, err
258 }
259 return &DiffVersion{
260 Dir: dir,
261 Name: name,
262 }, nil
263 }
264
265 func (v *DiffVersion) getObject(obj Object) (runtime.Object, error) {
266 switch v.Name {
267 case "LIVE":
268 return obj.Live(), nil
269 case "MERGED":
270 return obj.Merged()
271 }
272 return nil, fmt.Errorf("Unknown version: %v", v.Name)
273 }
274
275
276 func (v *DiffVersion) Print(name string, obj runtime.Object, printer Printer) error {
277 f, err := v.Dir.NewFile(name)
278 if err != nil {
279 return err
280 }
281 defer f.Close()
282 return printer.Print(obj, f)
283 }
284
285
286 type Directory struct {
287 Name string
288 }
289
290
291
292 func CreateDirectory(prefix string) (*Directory, error) {
293 name, err := os.MkdirTemp("", prefix+"-")
294 if err != nil {
295 return nil, err
296 }
297
298 return &Directory{
299 Name: name,
300 }, nil
301 }
302
303
304 func (d *Directory) NewFile(name string) (*os.File, error) {
305 return os.OpenFile(filepath.Join(d.Name, name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)
306 }
307
308
309 func (d *Directory) Delete() error {
310 return os.RemoveAll(d.Name)
311 }
312
313
314
315 type Object interface {
316 Live() runtime.Object
317 Merged() (runtime.Object, error)
318
319 Name() string
320 }
321
322
323
324 type InfoObject struct {
325 LocalObj runtime.Object
326 Info *resource.Info
327 Encoder runtime.Encoder
328 OpenAPIGetter openapi.OpenAPIResourcesGetter
329 OpenAPIV3Root openapi3.Root
330 Force bool
331 ServerSideApply bool
332 FieldManager string
333 ForceConflicts bool
334 genericiooptions.IOStreams
335 }
336
337 var _ Object = &InfoObject{}
338
339
340 func (obj InfoObject) Live() runtime.Object {
341 return obj.Info.Object
342 }
343
344
345
346 func (obj InfoObject) Merged() (runtime.Object, error) {
347 helper := resource.NewHelper(obj.Info.Client, obj.Info.Mapping).
348 DryRun(true).
349 WithFieldManager(obj.FieldManager)
350 if obj.ServerSideApply {
351 data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj)
352 if err != nil {
353 return nil, err
354 }
355 options := metav1.PatchOptions{
356 Force: &obj.ForceConflicts,
357 FieldManager: obj.FieldManager,
358 }
359 return helper.Patch(
360 obj.Info.Namespace,
361 obj.Info.Name,
362 types.ApplyPatchType,
363 data,
364 &options,
365 )
366 }
367
368
369 if obj.Live() == nil {
370
371 return helper.CreateWithOptions(
372 obj.Info.Namespace,
373 true,
374 obj.LocalObj,
375 &metav1.CreateOptions{},
376 )
377 }
378
379 var resourceVersion *string
380 if !obj.Force {
381 accessor, err := meta.Accessor(obj.Info.Object)
382 if err != nil {
383 return nil, err
384 }
385 str := accessor.GetResourceVersion()
386 resourceVersion = &str
387 }
388
389 modified, err := util.GetModifiedConfiguration(obj.LocalObj, false, unstructured.UnstructuredJSONScheme)
390 if err != nil {
391 return nil, err
392 }
393
394
395
396 patcher := &apply.Patcher{
397 Mapping: obj.Info.Mapping,
398 Helper: helper,
399 Overwrite: true,
400 BackOff: clockwork.NewRealClock(),
401 OpenAPIGetter: obj.OpenAPIGetter,
402 OpenAPIV3Root: obj.OpenAPIV3Root,
403 ResourceVersion: resourceVersion,
404 }
405
406 _, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, obj.ErrOut)
407 return result, err
408 }
409
410 func (obj InfoObject) Name() string {
411 group := ""
412 if obj.Info.Mapping.GroupVersionKind.Group != "" {
413 group = fmt.Sprintf("%v.", obj.Info.Mapping.GroupVersionKind.Group)
414 }
415 return group + fmt.Sprintf(
416 "%v.%v.%v.%v",
417 obj.Info.Mapping.GroupVersionKind.Version,
418 obj.Info.Mapping.GroupVersionKind.Kind,
419 obj.Info.Namespace,
420 obj.Info.Name,
421 )
422 }
423
424
425 func toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) {
426 if obj == nil {
427 return nil, nil
428 }
429 c, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject())
430 if err != nil {
431 return nil, fmt.Errorf("convert to unstructured: %w", err)
432 }
433 u := &unstructured.Unstructured{}
434 u.SetUnstructuredContent(c)
435 return u, nil
436 }
437
438
439
440
441
442
443
444 type Masker struct {
445 from *unstructured.Unstructured
446 to *unstructured.Unstructured
447 }
448
449 func NewMasker(from, to runtime.Object) (*Masker, error) {
450
451 f, err := toUnstructured(from)
452 if err != nil {
453 return nil, fmt.Errorf("convert to unstructured: %w", err)
454 }
455 t, err := toUnstructured(to)
456 if err != nil {
457 return nil, fmt.Errorf("convert to unstructured: %w", err)
458 }
459
460
461 m := &Masker{
462 from: f,
463 to: t,
464 }
465 if err := m.run(); err != nil {
466 return nil, fmt.Errorf("run masker: %w", err)
467 }
468 return m, nil
469 }
470
471
472 func (m Masker) dataFromUnstructured(u *unstructured.Unstructured) (map[string]interface{}, error) {
473 if u == nil {
474 return nil, nil
475 }
476 data, found, err := unstructured.NestedMap(u.UnstructuredContent(), "data")
477 if err != nil {
478 return nil, fmt.Errorf("get nested map: %w", err)
479 }
480 if !found {
481 return nil, nil
482 }
483 return data, nil
484 }
485
486
487 func (m *Masker) run() error {
488
489 from, err := m.dataFromUnstructured(m.from)
490 if err != nil {
491 return fmt.Errorf("extract 'data' field: %w", err)
492 }
493 to, err := m.dataFromUnstructured(m.to)
494 if err != nil {
495 return fmt.Errorf("extract 'data' field: %w", err)
496 }
497
498 for k := range from {
499
500
501
502 if _, ok := to[k]; ok {
503 if from[k] != to[k] {
504 from[k] = sensitiveMaskBefore
505 to[k] = sensitiveMaskAfter
506 continue
507 }
508 to[k] = sensitiveMaskDefault
509 }
510 from[k] = sensitiveMaskDefault
511 }
512 for k := range to {
513
514 if _, ok := from[k]; !ok {
515 to[k] = sensitiveMaskDefault
516 }
517 }
518
519
520 if m.from != nil && from != nil {
521 if err := unstructured.SetNestedMap(m.from.UnstructuredContent(), from, "data"); err != nil {
522 return fmt.Errorf("patch masked data: %w", err)
523 }
524 }
525 if m.to != nil && to != nil {
526 if err := unstructured.SetNestedMap(m.to.UnstructuredContent(), to, "data"); err != nil {
527 return fmt.Errorf("patch masked data: %w", err)
528 }
529 }
530 return nil
531 }
532
533
534 func (m *Masker) From() runtime.Object {
535 return m.from
536 }
537
538
539 func (m *Masker) To() runtime.Object {
540 return m.to
541 }
542
543
544 type Differ struct {
545 From *DiffVersion
546 To *DiffVersion
547 }
548
549 func NewDiffer(from, to string) (*Differ, error) {
550 differ := Differ{}
551 var err error
552 differ.From, err = NewDiffVersion(from)
553 if err != nil {
554 return nil, err
555 }
556 differ.To, err = NewDiffVersion(to)
557 if err != nil {
558 differ.From.Dir.Delete()
559 return nil, err
560 }
561
562 return &differ, nil
563 }
564
565
566 func (d *Differ) Diff(obj Object, printer Printer, showManagedFields bool) error {
567 from, err := d.From.getObject(obj)
568 if err != nil {
569 return err
570 }
571 to, err := d.To.getObject(obj)
572 if err != nil {
573 return err
574 }
575
576 if !showManagedFields {
577 from = omitManagedFields(from)
578 to = omitManagedFields(to)
579 }
580
581
582 if gvk := to.GetObjectKind().GroupVersionKind(); gvk.Version == "v1" && gvk.Kind == "Secret" {
583 m, err := NewMasker(from, to)
584 if err != nil {
585 return err
586 }
587 from, to = m.From(), m.To()
588 }
589
590 if err := d.From.Print(obj.Name(), from, printer); err != nil {
591 return err
592 }
593 if err := d.To.Print(obj.Name(), to, printer); err != nil {
594 return err
595 }
596 return nil
597 }
598
599 func omitManagedFields(o runtime.Object) runtime.Object {
600 a, err := meta.Accessor(o)
601 if err != nil {
602
603 return o
604 }
605 a.SetManagedFields(nil)
606 return o
607 }
608
609
610 func (d *Differ) Run(diff *DiffProgram) error {
611 return diff.Run(d.From.Dir.Name, d.To.Dir.Name)
612 }
613
614
615 func (d *Differ) TearDown() {
616 d.From.Dir.Delete()
617 d.To.Dir.Delete()
618 }
619
620 func isConflict(err error) bool {
621 return err != nil && errors.IsConflict(err)
622 }
623
624 func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
625 if len(args) != 0 {
626 return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
627 }
628
629 var err error
630
631 err = o.FilenameOptions.RequireFilenameOrKustomize()
632 if err != nil {
633 return err
634 }
635
636 o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd)
637 o.FieldManager = apply.GetApplyFieldManagerFlag(cmd, o.ServerSideApply)
638 o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)
639 if o.ForceConflicts && !o.ServerSideApply {
640 return fmt.Errorf("--force-conflicts only works with --server-side")
641 }
642
643 if !o.ServerSideApply {
644 o.OpenAPIGetter = f
645 if !cmdutil.OpenAPIV3Patch.IsDisabled() {
646 openAPIV3Client, err := f.OpenAPIV3Client()
647 if err == nil {
648 o.OpenAPIV3Root = openapi3.NewRoot(openAPIV3Client)
649 } else {
650 klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
651 }
652 }
653 }
654
655 o.DynamicClient, err = f.DynamicClient()
656 if err != nil {
657 return err
658 }
659
660 o.CmdNamespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
661 if err != nil {
662 return err
663 }
664
665 if cmdutil.GetFlagBool(cmd, "prune") {
666 mapper, err := f.ToRESTMapper()
667 if err != nil {
668 return err
669 }
670
671 resources, err := prune.ParseResources(mapper, cmdutil.GetFlagStringArray(cmd, "prune-allowlist"))
672 if err != nil {
673 return err
674 }
675 o.tracker = newTracker()
676 o.pruner = newPruner(o.DynamicClient, mapper, resources, o.Selector)
677 }
678
679 o.Builder = f.NewBuilder()
680 return nil
681 }
682
683
684
685
686 func (o *DiffOptions) Run() error {
687 differ, err := NewDiffer("LIVE", "MERGED")
688 if err != nil {
689 return err
690 }
691 defer differ.TearDown()
692
693 printer := Printer{}
694
695 r := o.Builder.
696 Unstructured().
697 VisitorConcurrency(o.Concurrency).
698 NamespaceParam(o.CmdNamespace).DefaultNamespace().
699 FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
700 LabelSelectorParam(o.Selector).
701 Flatten().
702 Do()
703 if err := r.Err(); err != nil {
704 return err
705 }
706
707 err = r.Visit(func(info *resource.Info, err error) error {
708 if err != nil {
709 return err
710 }
711
712 local := info.Object.DeepCopyObject()
713 for i := 1; i <= maxRetries; i++ {
714 if err = info.Get(); err != nil {
715 if !errors.IsNotFound(err) {
716 return err
717 }
718 info.Object = nil
719 }
720
721 force := i == maxRetries
722 if force {
723 klog.Warningf(
724 "Object (%v: %v) keeps changing, diffing without lock",
725 info.Object.GetObjectKind().GroupVersionKind(),
726 info.Name,
727 )
728 }
729 obj := InfoObject{
730 LocalObj: local,
731 Info: info,
732 Encoder: scheme.DefaultJSONEncoder(),
733 OpenAPIGetter: o.OpenAPIGetter,
734 OpenAPIV3Root: o.OpenAPIV3Root,
735 Force: force,
736 ServerSideApply: o.ServerSideApply,
737 FieldManager: o.FieldManager,
738 ForceConflicts: o.ForceConflicts,
739 IOStreams: o.Diff.IOStreams,
740 }
741
742 if o.tracker != nil {
743 o.tracker.MarkVisited(info)
744 }
745
746 err = differ.Diff(obj, printer, o.ShowManagedFields)
747 if !isConflict(err) {
748 break
749 }
750 }
751
752 apply.WarnIfDeleting(info.Object, o.Diff.ErrOut)
753
754 return err
755 })
756
757 if o.pruner != nil {
758 prunedObjs, err := o.pruner.pruneAll(o.tracker, o.CmdNamespace != "")
759 if err != nil {
760 klog.Warningf("pruning failed and could not be evaluated err: %v", err)
761 }
762
763
764
765 for _, p := range prunedObjs {
766 name, err := getObjectName(p)
767 if err != nil {
768 klog.Warningf("pruning failed and object name could not be retrieved: %v", err)
769 continue
770 }
771 if err := differ.From.Print(name, p, printer); err != nil {
772 return err
773 }
774 }
775 }
776
777 if err != nil {
778 return err
779 }
780
781 return differ.Run(o.Diff)
782 }
783
784
785 func (o *DiffOptions) Validate() error {
786 return nil
787 }
788
789 func getObjectName(obj runtime.Object) (string, error) {
790 gvk := obj.GetObjectKind().GroupVersionKind()
791 metadata, err := meta.Accessor(obj)
792 if err != nil {
793 return "", err
794 }
795 name := metadata.GetName()
796 ns := metadata.GetNamespace()
797
798 group := ""
799 if gvk.Group != "" {
800 group = fmt.Sprintf("%v.", gvk.Group)
801 }
802 return group + fmt.Sprintf(
803 "%v.%v.%v.%v",
804 gvk.Version,
805 gvk.Kind,
806 ns,
807 name,
808 ), nil
809 }
810
View as plain text