github.com/splunk/dan1-qbec@v0.7.3/internal/commands/diff.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package commands
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"sort"
    23  	"sync"
    24  
    25  	"github.com/ghodss/yaml"
    26  	"github.com/spf13/cobra"
    27  	"github.com/splunk/qbec/internal/diff"
    28  	"github.com/splunk/qbec/internal/model"
    29  	"github.com/splunk/qbec/internal/objsort"
    30  	"github.com/splunk/qbec/internal/remote"
    31  	"github.com/splunk/qbec/internal/sio"
    32  	"github.com/splunk/qbec/internal/types"
    33  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    34  )
    35  
    36  type diffIgnores struct {
    37  	allAnnotations  bool
    38  	allLabels       bool
    39  	annotationNames []string
    40  	labelNames      []string
    41  }
    42  
    43  func (di diffIgnores) preprocess(obj *unstructured.Unstructured) {
    44  	if di.allLabels || len(di.labelNames) > 0 {
    45  		labels := obj.GetLabels()
    46  		if labels == nil {
    47  			labels = map[string]string{}
    48  		}
    49  		if di.allLabels {
    50  			labels = map[string]string{}
    51  		} else {
    52  			for _, l := range di.labelNames {
    53  				delete(labels, l)
    54  			}
    55  		}
    56  		obj.SetLabels(labels)
    57  	}
    58  	if di.allAnnotations || len(di.annotationNames) > 0 {
    59  		annotations := obj.GetAnnotations()
    60  		if annotations == nil {
    61  			annotations = map[string]string{}
    62  		}
    63  		if di.allAnnotations {
    64  			annotations = map[string]string{}
    65  		} else {
    66  			for _, l := range di.annotationNames {
    67  				delete(annotations, l)
    68  			}
    69  		}
    70  		obj.SetAnnotations(annotations)
    71  	}
    72  }
    73  
    74  type diffStats struct {
    75  	l         sync.Mutex
    76  	Additions []string `json:"additions,omitempty"`
    77  	Changes   []string `json:"changes,omitempty"`
    78  	Deletions []string `json:"deletions,omitempty"`
    79  	SameCount int      `json:"same,omitempty"`
    80  	Errors    []string `json:"errors,omitempty"`
    81  }
    82  
    83  func (d *diffStats) added(s string) {
    84  	d.l.Lock()
    85  	defer d.l.Unlock()
    86  	d.Additions = append(d.Additions, s)
    87  }
    88  
    89  func (d *diffStats) changed(s string) {
    90  	d.l.Lock()
    91  	defer d.l.Unlock()
    92  	d.Changes = append(d.Changes, s)
    93  }
    94  
    95  func (d *diffStats) deleted(s string) {
    96  	d.l.Lock()
    97  	defer d.l.Unlock()
    98  	d.Deletions = append(d.Deletions, s)
    99  }
   100  
   101  func (d *diffStats) same(s string) {
   102  	d.l.Lock()
   103  	defer d.l.Unlock()
   104  	d.SameCount++
   105  }
   106  
   107  func (d *diffStats) errors(s string) {
   108  	d.l.Lock()
   109  	defer d.l.Unlock()
   110  	d.Errors = append(d.Errors, s)
   111  }
   112  
   113  func (d *diffStats) done() {
   114  	sort.Strings(d.Additions)
   115  	sort.Strings(d.Changes)
   116  	sort.Strings(d.Errors)
   117  }
   118  
   119  type differ struct {
   120  	w           io.Writer
   121  	client      Client
   122  	opts        diff.Options
   123  	stats       diffStats
   124  	ignores     diffIgnores
   125  	showSecrets bool
   126  	verbose     int
   127  }
   128  
   129  func (d *differ) names(ob model.K8sMeta) (name, leftName, rightName string) {
   130  	name = d.client.DisplayName(ob)
   131  	leftName = "live " + name
   132  	rightName = "config " + name
   133  	return
   134  }
   135  
   136  type namedUn struct {
   137  	name string
   138  	obj  *unstructured.Unstructured
   139  }
   140  
   141  // writeDiff writes the diff between the left and right objects. Either of these
   142  // objects may be nil in which case the supplied object text is diffed against
   143  // a blank string. Care must be taken to ensure that only a single write is made to the writer for every invocation.
   144  // Otherwise output will be interleaved across diffs.
   145  func (d *differ) writeDiff(name string, left, right namedUn) (finalErr error) {
   146  	asYaml := func(obj interface{}) (string, error) {
   147  		b, err := yaml.Marshal(obj)
   148  		if err != nil {
   149  			return "", err
   150  		}
   151  		return string(b), nil
   152  	}
   153  	addLeader := func(s, leader string) string {
   154  		l := fmt.Sprintf("#\n# %s\n#\n", leader)
   155  		return l + s
   156  	}
   157  	defer func() {
   158  		if finalErr != nil {
   159  			d.stats.errors(name)
   160  			sio.Errorf("error diffing %s, %v\n", name, finalErr)
   161  		}
   162  	}()
   163  
   164  	fileOpts := d.opts
   165  	fileOpts.LeftName = left.name
   166  	fileOpts.RightName = right.name
   167  	switch {
   168  	case left.obj == nil && &right.obj == nil:
   169  		return fmt.Errorf("internal error: both left and right objects were nil for diff")
   170  	case left.obj != nil && right.obj != nil:
   171  		b, err := diff.Objects(left.obj, right.obj, fileOpts)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		if len(b) == 0 {
   176  			if d.verbose > 0 {
   177  				fmt.Fprintf(d.w, "%s unchanged\n", name)
   178  			}
   179  			d.stats.same(name)
   180  		} else {
   181  			fmt.Fprintln(d.w, string(b))
   182  			d.stats.changed(name)
   183  		}
   184  	case left.obj == nil:
   185  		rightContent, err := asYaml(right.obj)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		leaderComment := "object doesn't exist on the server"
   190  		if right.obj.GetName() == "" {
   191  			leaderComment += " (generated name)"
   192  		}
   193  		rightContent = addLeader(rightContent, leaderComment)
   194  		b, err := diff.Strings("", rightContent, fileOpts)
   195  		if err != nil {
   196  			return err
   197  		}
   198  		fmt.Fprintln(d.w, string(b))
   199  		d.stats.added(name)
   200  	default:
   201  		leftContent, err := asYaml(left.obj)
   202  		if err != nil {
   203  			return err
   204  		}
   205  		leftContent = addLeader(leftContent, "object doesn't exist locally")
   206  		b, err := diff.Strings(leftContent, "", fileOpts)
   207  		if err != nil {
   208  			return err
   209  		}
   210  		fmt.Fprintln(d.w, string(b))
   211  		d.stats.deleted(name)
   212  	}
   213  	return nil
   214  }
   215  
   216  // diff diffs the supplied object with its remote version and writes output to its writer.
   217  // The local version is found by downcasting the supplied metadata to a local object.
   218  // This cast should succeed for all but the deletion use case.
   219  func (d *differ) diff(ob model.K8sMeta) error {
   220  	name, leftName, rightName := d.names(ob)
   221  
   222  	var remoteObject *unstructured.Unstructured
   223  	var err error
   224  
   225  	if ob.GetName() != "" {
   226  		remoteObject, err = d.client.Get(ob)
   227  		if err != nil && err != remote.ErrNotFound {
   228  			d.stats.errors(name)
   229  			sio.Errorf("error fetching %s, %v\n", name, err)
   230  			return err
   231  		}
   232  	}
   233  
   234  	fixup := func(u *unstructured.Unstructured) *unstructured.Unstructured {
   235  		if u == nil {
   236  			return u
   237  		}
   238  		if !d.showSecrets {
   239  			u, _ = types.HideSensitiveInfo(u)
   240  		}
   241  		d.ignores.preprocess(u)
   242  		return u
   243  	}
   244  
   245  	var left, right *unstructured.Unstructured
   246  	if remoteObject != nil {
   247  		var source string
   248  		left, source = remote.GetPristineVersionForDiff(remoteObject)
   249  		leftName += " (source: " + source + ")"
   250  	}
   251  	left = fixup(left)
   252  
   253  	if r, ok := ob.(model.K8sObject); ok {
   254  		right = fixup(r.ToUnstructured())
   255  	}
   256  	return d.writeDiff(name, namedUn{name: leftName, obj: left}, namedUn{name: rightName, obj: right})
   257  }
   258  
   259  // diffLocal adapts the diff method to run as a parallel worker.
   260  func (d *differ) diffLocal(ob model.K8sLocalObject) error {
   261  	return d.diff(ob)
   262  }
   263  
   264  type diffCommandConfig struct {
   265  	*Config
   266  	showDeletions bool
   267  	showSecrets   bool
   268  	parallel      int
   269  	contextLines  int
   270  	di            diffIgnores
   271  	filterFunc    func() (filterParams, error)
   272  }
   273  
   274  func doDiff(args []string, config diffCommandConfig) error {
   275  	if len(args) != 1 {
   276  		return newUsageError("exactly one environment required")
   277  	}
   278  
   279  	env := args[0]
   280  	if env == model.Baseline {
   281  		return newUsageError("cannot diff baseline environment, use a real environment")
   282  	}
   283  	fp, err := config.filterFunc()
   284  	if err != nil {
   285  		return err
   286  	}
   287  
   288  	client, err := config.Client(env)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	objects, err := filteredObjects(config.Config, env, client.ObjectKey, fp)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	var lister lister = &stubLister{}
   299  	var all []model.K8sLocalObject
   300  	var retainObjects []model.K8sLocalObject
   301  	if config.showDeletions {
   302  		all, err = allObjects(config.Config, env)
   303  		if err != nil {
   304  			return err
   305  		}
   306  		for _, o := range all {
   307  			if o.GetName() != "" {
   308  				retainObjects = append(retainObjects, o)
   309  			}
   310  		}
   311  		var scope remote.ListQueryScope
   312  		lister, scope, err = newRemoteLister(client, all, config.app.DefaultNamespace(env))
   313  		if err != nil {
   314  			return err
   315  		}
   316  		lister.start(remote.ListQueryConfig{
   317  			Application:    config.App().Name(),
   318  			Tag:            config.App().Tag(),
   319  			Environment:    env,
   320  			KindFilter:     fp.kindFilter,
   321  			ListQueryScope: scope,
   322  		})
   323  	}
   324  
   325  	objects = objsort.Sort(objects, sortConfig(client.IsNamespaced))
   326  
   327  	// since the 0 value of context is turned to 3 by the diff library,
   328  	// special case to turn 0 into a negative number so that zero means zero.
   329  	if config.contextLines == 0 {
   330  		config.contextLines = -1
   331  	}
   332  	opts := diff.Options{Context: config.contextLines, Colorize: config.Colorize()}
   333  
   334  	w := &lockWriter{Writer: config.Stdout()}
   335  	d := &differ{
   336  		w:           w,
   337  		client:      client,
   338  		opts:        opts,
   339  		ignores:     config.di,
   340  		showSecrets: config.showSecrets,
   341  		verbose:     config.Verbosity(),
   342  	}
   343  	dErr := runInParallel(objects, d.diffLocal, config.parallel)
   344  
   345  	var listErr error
   346  	if dErr == nil {
   347  		extra, err := lister.deletions(retainObjects, fp.Includes)
   348  		if err != nil {
   349  			listErr = err
   350  		} else {
   351  			for _, ob := range extra {
   352  				if err := d.diff(ob); err != nil {
   353  					return err
   354  				}
   355  			}
   356  		}
   357  	}
   358  
   359  	d.stats.done()
   360  	printStats(d.w, &d.stats)
   361  	numDiffs := len(d.stats.Additions) + len(d.stats.Changes) + len(d.stats.Deletions)
   362  
   363  	switch {
   364  	case dErr != nil:
   365  		return dErr
   366  	case listErr != nil:
   367  		return listErr
   368  	case numDiffs > 0:
   369  		return fmt.Errorf("%d object(s) different", numDiffs)
   370  	default:
   371  		return nil
   372  	}
   373  }
   374  
   375  func newDiffCommand(cp ConfigProvider) *cobra.Command {
   376  	cmd := &cobra.Command{
   377  		Use:     "diff <environment>",
   378  		Short:   "diff one or more components against objects in a Kubernetes cluster",
   379  		Example: diffExamples(),
   380  	}
   381  
   382  	config := diffCommandConfig{
   383  		filterFunc: addFilterParams(cmd, true),
   384  	}
   385  	cmd.Flags().BoolVar(&config.showDeletions, "show-deletes", true, "include deletions in diff")
   386  	cmd.Flags().IntVar(&config.contextLines, "context", 3, "context lines for diff")
   387  	cmd.Flags().IntVar(&config.parallel, "parallel", 5, "number of parallel routines to run")
   388  	cmd.Flags().BoolVarP(&config.showSecrets, "show-secrets", "S", false, "do not obfuscate secret values in the diff")
   389  	cmd.Flags().BoolVar(&config.di.allAnnotations, "ignore-all-annotations", false, "remove all annotations from objects before diff")
   390  	cmd.Flags().StringArrayVar(&config.di.annotationNames, "ignore-annotation", nil, "remove specific annotation from objects before diff")
   391  	cmd.Flags().BoolVar(&config.di.allLabels, "ignore-all-labels", false, "remove all labels from objects before diff")
   392  	cmd.Flags().StringArrayVar(&config.di.labelNames, "ignore-label", nil, "remove specific label from objects before diff")
   393  
   394  	cmd.RunE = func(c *cobra.Command, args []string) error {
   395  		config.Config = cp()
   396  		return wrapError(doDiff(args, config))
   397  	}
   398  	return cmd
   399  }