-
Jessica Yuen authored
This change enables the user to diff between two environments that are either local or remote. i.e., `kubecfg diff local:dev local:prod` will diff between the expanded templates for each environment on disk. `kubecfg diff remote:dev remote:prod` will diff between two remote environment clusters. It does this by first expanding the component templates of each environment. Then, the live objects are fetched from each of the clusters and the diff is performed against the live objects. `kubecfg diff local:dev remote:prod` is also an option. This will diff between the expanded templates for 'dev' on disk and the live objects on 'prod's server.
2687c6d8
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
diff.go 7.36 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 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
}