Skip to content
Snippets Groups Projects
  • Alex Clemmer's avatar
    Make delete, diff, show, update, and validate aware of ksonnet apps · af3f0f6c
    Alex Clemmer authored
    The ksonnet.next design doc specifies the core kubecfg verbs (i.e., the
    ones listed above) to all have the form:
    
      kubecfg <verb> [<env-name>|-f <file-or-dir>]
    
    That is to say, each of these should be able to take either an
    environment name, or a `-f` flag with a list of files and directories to
    apply `verb` on. In the case of the environment, we will apply `verb` to
    every component in the `components/` directory.
    
    This commit implements this behavior for all these verbs.
    af3f0f6c
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
diff.go 4.93 KiB
// Copyright 2017 The kubecfg authors
//
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.

package cmd

import (
	"fmt"
	"io"
	"os"
	"sort"

	"github.com/mattn/go-isatty"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/yudai/gojsondiff"
	"github.com/yudai/gojsondiff/formatter"
	"k8s.io/apimachinery/pkg/api/errors"

	"github.com/ksonnet/kubecfg/utils"
)

const flagDiffStrategy = "diff-strategy"

var ErrDiffFound = fmt.Errorf("Differences found.")

func init() {
	addEnvCmdFlags(diffCmd)
	diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
	RootCmd.AddCommand(diffCmd)
}

var diffCmd = &cobra.Command{
	Use:   "diff [<env>|-f <file-or-dir>]",
	Short: "Display differences between server and local config",
	RunE: func(cmd *cobra.Command, args []string) error {
		out := cmd.OutOrStdout()
		flags := cmd.Flags()
		diffStrategy, err := flags.GetString(flagDiffStrategy)
		if err != nil {
			return err
		}

		files, err := getFiles(cmd, args)
		if err != nil {
			return err
		}

		vm, err := newExpander(cmd)
		if err != nil {
			return err
		}

		objs, err := vm.Expand(files)
		if err != nil {
			return err
		}

		clientpool, disco, err := restClientPool(cmd)
		if err != nil {
			return err
		}

		defaultNs, _, err := clientConfig.Namespace()
		if err != nil {
			return err
		}

		sort.Sort(utils.AlphabeticalOrder(objs))

		diffFound := false
		for _, obj := range objs {
			desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(disco, obj), utils.FqName(obj))
			log.Debugf("Fetching ", desc)

			c, err := utils.ClientForResource(clientpool, disco, obj, defaultNs)
			if err != nil {
				return err
			}

			liveObj, err := c.Get(obj.GetName())
			if err != nil && errors.IsNotFound(err) {
				log.Debugf("%s doesn't exist on the server", desc)
				liveObj = nil
			} else if err != nil {
				return fmt.Errorf("Error fetching %s: %v", desc, err)
			}

			fmt.Fprintln(out, "---")
			fmt.Fprintf(out, "- live %s\n+ config %s\n", desc, desc)
			if liveObj == nil {
				fmt.Fprintf(out, "%s doesn't exist on server\n", desc)
				diffFound = true
				continue
			}

			liveObjObject := liveObj.Object
			if diffStrategy == "subset" {
				liveObjObject = removeMapFields(obj.Object, liveObjObject)
			}
			diff := gojsondiff.New().CompareObjects(liveObjObject, obj.Object)

			if diff.Modified() {
				diffFound = true
				fcfg := formatter.AsciiFormatterConfig{
					Coloring: istty(out),
				}
				formatter := formatter.NewAsciiFormatter(liveObjObject, fcfg)
				text, err := formatter.Format(diff)
				if err != nil {
					return err
				}
				fmt.Fprintf(out, "%s", text)
			} else {
				fmt.Fprintf(out, "%s unchanged\n", desc)
			}
		}

		if diffFound {
			return ErrDiffFound
		}
		return nil
	},
	Long: `Display differences between server and local configuration.

ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
files.`,
	Example: `  # Show diff between resources described in a local ksonnet application and
  # the cluster referenced by the 'dev' environment. Can be used in any
  # subdirectory of the application.
  ksonnet diff -e=dev

  # Show diff between resources described in a YAML file and the cluster
  # referenced in '$KUBECONFIG'.
  ksonnet diff -f ./pod.yaml

  # Show diff between resources described in a YAML file and the cluster
  # referred to by './kubeconfig'.
  ksonnet diff --kubeconfig=./kubeconfig -f ./pod.yaml`,
}

func removeFields(config, live interface{}) interface{} {
	switch c := config.(type) {
	case map[string]interface{}:
		return removeMapFields(c, live.(map[string]interface{}))
	case []interface{}:
		return removeListFields(c, live.([]interface{}))
	default:
		return live
	}
}

func removeMapFields(config, live map[string]interface{}) map[string]interface{} {
	result := map[string]interface{}{}
	for k, v1 := range config {
		v2, ok := live[k]
		if !ok {
			continue
		}
		result[k] = removeFields(v1, v2)
	}
	return result
}

func removeListFields(config, live []interface{}) []interface{} {
	// If live is longer than config, then the extra elements at the end of the
	// list will be returned as is so they appear in the diff.
	result := make([]interface{}, 0, len(live))
	for i, v2 := range live {
		if len(config) > i {
			result = append(result, removeFields(config[i], v2))
		} else {
			result = append(result, v2)
		}
	}
	return result
}

func istty(w io.Writer) bool {
	if f, ok := w.(*os.File); ok {
		return isatty.IsTerminal(f.Fd())
	}
	return false
}