// 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 kubecfg import ( "fmt" "io" "os" "sort" isatty "github.com/mattn/go-isatty" log "github.com/sirupsen/logrus" "github.com/yudai/gojsondiff" "github.com/yudai/gojsondiff/formatter" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "github.com/ksonnet/ksonnet/utils" ) var ErrDiffFound = fmt.Errorf("Differences found.") // DiffCmd is an interface containing a set of functions that allow diffing // between two sets of data containing Kubernetes resouces. type DiffCmd interface { Run(out io.Writer) error } // Diff is a base representation of the `diff` functionality. type Diff struct { DiffStrategy string } // Client holds the necessary information to connect with a remote Kubernetes // cluster. type Client struct { ClientPool dynamic.ClientPool Discovery discovery.DiscoveryInterface Namespace string // Name of the remote client to identify changes against. This field is Optional. Name string // APIObjects are the Kubernetes objects being diffed against APIObjects []*unstructured.Unstructured } // LocalEnv holds the local Kubernetes objects for an environment and relevant // environment details, such as, environment name type LocalEnv struct { Name string APIObjects []*unstructured.Unstructured } // --------------------------------------------------------------------------- // DiffRemoteCmd extends DiffCmd and is meant to represent diffing between some // set of Kubernetes objects and the Kubernetes objects located on a remote // client. type DiffRemoteCmd struct { Diff Client *Client } func (c *DiffRemoteCmd) Run(out io.Writer) error { const ( local = "config" remote = "live" ) _, liveObjs, err := getLiveObjs(c.Client) if err != nil { return err } return diffAll(c.Client.APIObjects, liveObjs, local, remote, c.DiffStrategy, &c.Client.Discovery, true, out) } // --------------------------------------------------------------------------- // DiffLocalCmd extends DiffCmd and is meant to represent diffing between two // sets of Kubernetes objects. type DiffLocalCmd struct { Diff Env1 *LocalEnv Env2 *LocalEnv } func (c *DiffLocalCmd) Run(out io.Writer) error { m := map[string]*unstructured.Unstructured{} for _, b := range c.Env2.APIObjects { m[hash(nil, b, true)] = b } return diffAll(c.Env1.APIObjects, m, c.Env1.Name, c.Env2.Name, c.DiffStrategy, nil, true, out) } // --------------------------------------------------------------------------- // DiffRemotesCmd extends DiffCmd and is meant to represent diffing between the // Kubernetes objects on two remote clients. type DiffRemotesCmd struct { Diff ClientA *Client ClientB *Client } func (c *DiffRemotesCmd) Run(out io.Writer) error { liveObjsA, _, err := getLiveObjs(c.ClientA) if err != nil { return err } _, liveObjsB, err := getLiveObjs(c.ClientB) if err != nil { return err } return diffAll(liveObjsA, liveObjsB, c.ClientA.Name, c.ClientB.Name, c.DiffStrategy, &c.ClientA.Discovery, false, out) } // --------------------------------------------------------------------------- func diffAll(a []*unstructured.Unstructured, b map[string]*unstructured.Unstructured, aName, bName, strategy string, discovery *discovery.DiscoveryInterface, fqName bool, out io.Writer) error { sort.Sort(utils.AlphabeticalOrder(a)) diffFound := false for _, o := range a { desc := hash(discovery, o, fqName) var bObj map[string]interface{} if b[desc] != nil { bObj = b[desc].Object } var err error log.Debugf("Diffing %s\nA: %s\nB: %s\n", desc, o.Object, bObj) diffFound, err = diff(desc, aName, bName, strategy, o.Object, bObj, out) if err != nil { return err } } if diffFound { return ErrDiffFound } return nil } func diff(desc, aName, bName, strategy string, aObj, bObj map[string]interface{}, out io.Writer) (diffFound bool, err error) { fmt.Fprintln(out, "---") fmt.Fprintf(out, "- %s %s\n+ %s %s\n", bName, desc, aName, desc) if bObj == nil { fmt.Fprintf(out, "%s doesn't exist on %s\n", desc, bName) return true, nil } if strategy == "subset" { bObj = removeMapFields(aObj, bObj) } diff := gojsondiff.New().CompareObjects(bObj, aObj) if diff.Modified() { fcfg := formatter.AsciiFormatterConfig{ Coloring: istty(out), } formatter := formatter.NewAsciiFormatter(bObj, fcfg) text, err := formatter.Format(diff) if err != nil { return true, err } fmt.Fprintf(out, "%s", text) return true, nil } fmt.Fprintf(out, "%s unchanged\n", desc) return false, nil } // hash serves as an identifier for the Kubernetes resource. func hash(discovery *discovery.DiscoveryInterface, obj *unstructured.Unstructured, fqName bool) string { name := obj.GetName() if fqName { name = utils.FqName(obj) } if discovery == nil { return fmt.Sprintf("%s %s", utils.GroupVersionKindFor(obj), name) } return fmt.Sprintf("%s %s", utils.ResourceNameFor(*discovery, obj), name) } func getLiveObjs(client *Client) ([]*unstructured.Unstructured, map[string]*unstructured.Unstructured, error) { var liveObjs []*unstructured.Unstructured liveObjsMap := map[string]*unstructured.Unstructured{} for _, obj := range client.APIObjects { desc := hash(&client.Discovery, obj, true) log.Debugf("Fetching %s", desc) client, err := utils.ClientForResource(client.ClientPool, client.Discovery, obj, client.Namespace) if err != nil { return nil, nil, err } liveObj, err := client.Get(obj.GetName()) if err != nil && errors.IsNotFound(err) { log.Debugf("%s doesn't exist on the server", desc) continue } else if err != nil { return nil, nil, fmt.Errorf("Error fetching %s: %v", desc, err) } liveObjs = append(liveObjs, liveObj) liveObjsMap[desc] = liveObj } return liveObjs, liveObjsMap, nil } 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 }