From 2687c6d8b0f7eccaa29742105928efe1e1fa6b62 Mon Sep 17 00:00:00 2001
From: Jessica Yuen <im.jessicayuen@gmail.com>
Date: Tue, 24 Oct 2017 15:10:12 -0700
Subject: [PATCH] Support `diff` between two environments

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.
---
 cmd/diff.go          | 255 +++++++++++++++++++++++++++++++++++++++----
 cmd/root.go          |  28 +++--
 cmd/root_test.go     |   3 +-
 pkg/kubecfg/apply.go |   4 +-
 pkg/kubecfg/diff.go  | 216 ++++++++++++++++++++++++++++--------
 utils/meta.go        |   6 +
 6 files changed, 432 insertions(+), 80 deletions(-)

diff --git a/cmd/diff.go b/cmd/diff.go
index b2ecefe2..bddf771e 100644
--- a/cmd/diff.go
+++ b/cmd/diff.go
@@ -18,8 +18,14 @@ package cmd
 import (
 	"fmt"
 	"os"
+	"strings"
+
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/dynamic"
 
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/client-go/tools/clientcmd"
 
 	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
@@ -35,22 +41,14 @@ func init() {
 }
 
 var diffCmd = &cobra.Command{
-	Use:   "diff [env-name] [-f <file-or-dir>]",
-	Short: "Display differences between server and local config",
+	Use:   "diff [<env1> [<env2>]] [-f <file-or-dir>]",
+	Short: "Display differences between server and local config, or server and server config",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		if len(args) > 1 {
-			return fmt.Errorf("'diff' takes at most a single argument, that is the name of the environment")
+		if len(args) > 2 {
+			return fmt.Errorf("'diff' takes at most two arguments, that are the name of the environments")
 		}
 
 		flags := cmd.Flags()
-		var err error
-
-		c := kubecfg.DiffCmd{}
-
-		c.DiffStrategy, err = flags.GetString(flagDiffStrategy)
-		if err != nil {
-			return err
-		}
 
 		cwd, err := os.Getwd()
 		if err != nil {
@@ -58,36 +56,55 @@ var diffCmd = &cobra.Command{
 		}
 		wd := metadata.AbsPath(cwd)
 
-		envSpec, err := parseEnvCmd(cmd, args)
+		files, err := flags.GetStringArray(flagFile)
 		if err != nil {
 			return err
 		}
 
-		c.ClientPool, c.Discovery, err = restClientPool(cmd, envSpec.env)
-		if err != nil {
-			return err
+		var env1 *string
+		if len(args) > 0 {
+			env1 = &args[0]
+		}
+
+		var env2 *string
+		if len(args) > 1 {
+			env2 = &args[1]
 		}
 
-		c.Namespace, err = namespace()
+		diffStrategy, err := flags.GetString(flagDiffStrategy)
 		if err != nil {
 			return err
 		}
 
-		objs, err := expandEnvCmdObjs(cmd, envSpec, wd)
+		c, err := initDiffCmd(cmd, wd, env1, env2, files, diffStrategy)
 		if err != nil {
 			return err
 		}
 
-		return c.Run(objs, cmd.OutOrStdout())
+		return c.Run(cmd.OutOrStdout())
 	},
-	Long: `Display differences between server and local configuration.
+	Long: `Display differences between server and local configuration, or server and server
+configurations.
 
 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.
-  ks diff dev
+	Example: `  # Show diff between resources described in a the local 'dev' environment
+  # specified by the ksonnet application and the remote cluster referenced by
+  # the same 'dev' environment. Can be used in any subdirectory of the application.
+  ksonnet diff dev
+
+  # Show diff between resources at remote clusters. This requires ksonnet
+  # application defined environments. Diff between the cluster defined at the
+  # 'us-west/dev' environment, and the cluster defined at the 'us-west/prod'
+  # environment. Can be used in any subdirectory of the application.
+  ksonnet diff remote:us-west/dev remote:us-west/prod
+
+  # Show diff between resources at a remote and a local cluster. This requires
+  # ksonnet application defined environments. Diff between the cluster defined
+  # at the 'us-west/dev' environment, and the cluster defined at the
+  # 'us-west/prod' environment. Can be used in any subdirectory of the
+  # application.
+  ksonnet diff local:us-west/dev remote:us-west/prod
 
   # Show diff between resources described in a YAML file and the cluster
   # referenced in '$KUBECONFIG'.
@@ -101,3 +118,193 @@ files.`,
   # referred to by './kubeconfig'.
   ks diff --kubeconfig=./kubeconfig -f ./pod.yaml`,
 }
+
+func initDiffCmd(cmd *cobra.Command, wd metadata.AbsPath, envFq1, envFq2 *string, files []string, diffStrategy string) (kubecfg.DiffCmd, error) {
+	const (
+		remote = "remote"
+		local  = "local"
+	)
+
+	if envFq2 == nil {
+		return initDiffSingleEnv(*envFq1, diffStrategy, files, cmd, wd)
+	}
+
+	// expect envs to be of the format local:myenv or remote:myenv
+	env1 := strings.SplitN(*envFq1, ":", 2)
+	env2 := strings.SplitN(*envFq2, ":", 2)
+
+	// validation
+	if len(env1) < 2 || len(env2) < 2 || (env1[0] != local && env1[0] != remote) || (env2[0] != local && env2[0] != remote) {
+		return nil, fmt.Errorf("<env> must be prefaced by %s: or %s:, ex: %s:us-west/prod", local, remote, remote)
+	}
+	if len(files) > 0 {
+		return nil, fmt.Errorf("'-f' is not currently supported for multiple environments")
+	}
+
+	manager, err := metadata.Find(wd)
+	if err != nil {
+		return nil, err
+	}
+	componentPaths, err := manager.ComponentPaths()
+	if err != nil {
+		return nil, err
+	}
+	baseObj := constructBaseObj(componentPaths)
+
+	if env1[0] == local && env2[0] == local {
+		return initDiffLocalCmd(env1[1], env2[1], diffStrategy, baseObj, cmd, manager)
+	}
+
+	if env1[0] == remote && env2[0] == remote {
+		return initDiffRemotesCmd(env1[1], env2[1], diffStrategy, baseObj, cmd, manager)
+	}
+
+	localEnv := env1[1]
+	remoteEnv := env2[1]
+	if env1[0] == remote {
+		localEnv = env2[1]
+		remoteEnv = env1[1]
+	}
+	return initDiffRemoteCmd(localEnv, remoteEnv, diffStrategy, baseObj, cmd, manager)
+}
+
+// initDiffSingleEnv sets up configurations for diffing using one environment
+func initDiffSingleEnv(env, diffStrategy string, files []string, cmd *cobra.Command, wd metadata.AbsPath) (kubecfg.DiffCmd, error) {
+	c := kubecfg.DiffRemoteCmd{}
+	c.DiffStrategy = diffStrategy
+	c.Client = &kubecfg.Client{}
+	var err error
+
+	if strings.HasPrefix(env, "remote:") || strings.HasPrefix(env, "local:") {
+		return nil, fmt.Errorf("single <env> argument with prefix 'local:' or 'remote:' not allowed")
+	}
+
+	envSpec := &envSpec{env: &env, files: files}
+	c.Client.APIObjects, err = expandEnvCmdObjs(cmd, envSpec, wd)
+	if err != nil {
+		return nil, err
+	}
+
+	c.Client.ClientPool, c.Client.Discovery, err = restClientPool(cmd, envSpec.env)
+	if err != nil {
+		return nil, err
+	}
+
+	c.Client.Namespace, err = namespace()
+	if err != nil {
+		return nil, err
+	}
+
+	return &c, nil
+}
+
+// initDiffLocalCmd sets up configurations for diffing between two sets of expanded Kubernetes objects locally
+func initDiffLocalCmd(env1, env2, diffStrategy, baseObj string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) {
+	c := kubecfg.DiffLocalCmd{}
+	c.DiffStrategy = diffStrategy
+	var err error
+
+	c.Env1 = &kubecfg.LocalEnv{}
+	c.Env1.Name = env1
+	c.Env1.APIObjects, err = expandEnvObjs(cmd, c.Env1.Name, baseObj, m)
+	if err != nil {
+		return nil, err
+	}
+
+	c.Env2 = &kubecfg.LocalEnv{}
+	c.Env2.Name = env2
+	c.Env2.APIObjects, err = expandEnvObjs(cmd, c.Env2.Name, baseObj, m)
+	if err != nil {
+		return nil, err
+	}
+
+	return &c, nil
+}
+
+// initDiffRemotesCmd sets up configurations for diffing between objects on two remote clusters
+func initDiffRemotesCmd(env1, env2, diffStrategy, baseObj string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) {
+	c := kubecfg.DiffRemotesCmd{}
+	c.DiffStrategy = diffStrategy
+
+	c.ClientA = &kubecfg.Client{}
+	c.ClientB = &kubecfg.Client{}
+
+	c.ClientA.Name = env1
+	c.ClientB.Name = env2
+
+	var err error
+	c.ClientA.APIObjects, err = expandEnvObjs(cmd, c.ClientA.Name, baseObj, m)
+	if err != nil {
+		return nil, err
+	}
+	c.ClientB.APIObjects, err = expandEnvObjs(cmd, c.ClientB.Name, baseObj, m)
+	if err != nil {
+		return nil, err
+	}
+
+	c.ClientA.ClientPool, c.ClientA.Discovery, c.ClientA.Namespace, err = setupClientConfig(&c.ClientA.Name, cmd)
+	if err != nil {
+		return nil, err
+	}
+	c.ClientB.ClientPool, c.ClientB.Discovery, c.ClientB.Namespace, err = setupClientConfig(&c.ClientB.Name, cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	return &c, nil
+}
+
+// initDiffRemoteCmd sets up configurations for diffing between local objects and objects on a remote cluster
+func initDiffRemoteCmd(localEnv, remoteEnv, diffStrategy, baseObj string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) {
+	c := kubecfg.DiffRemoteCmd{}
+	c.DiffStrategy = diffStrategy
+	c.Client = &kubecfg.Client{}
+
+	var err error
+	c.Client.APIObjects, err = expandEnvObjs(cmd, localEnv, baseObj, m)
+	if err != nil {
+		return nil, err
+	}
+
+	c.Client.ClientPool, c.Client.Discovery, c.Client.Namespace, err = setupClientConfig(&remoteEnv, cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	return &c, nil
+}
+
+func setupClientConfig(env *string, cmd *cobra.Command) (dynamic.ClientPool, discovery.DiscoveryInterface, string, error) {
+	overrides := clientcmd.ConfigOverrides{}
+	loadingRules := *clientcmd.NewDefaultClientConfigLoadingRules()
+	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
+	config := clientcmd.NewInteractiveDeferredLoadingClientConfig(&loadingRules, &overrides, os.Stdin)
+
+	clientPool, discovery, err := restClient(cmd, env, config, overrides)
+	if err != nil {
+		return nil, nil, "", err
+	}
+
+	namespace, err := namespaceFor(config, overrides)
+	if err != nil {
+		return nil, nil, "", err
+	}
+
+	return clientPool, discovery, namespace, nil
+}
+
+// expandEnvObjs finds and expands templates for an environment
+func expandEnvObjs(cmd *cobra.Command, env, baseObj string, manager metadata.Manager) ([]*unstructured.Unstructured, error) {
+	expander, err := newExpander(cmd)
+	if err != nil {
+		return nil, err
+	}
+
+	libPath, envLibPath, envComponentPath := manager.LibPaths(env)
+	expander.FlagJpath = append([]string{string(libPath), string(envLibPath)}, expander.FlagJpath...)
+	expander.ExtCodes = append([]string{baseObj}, expander.ExtCodes...)
+
+	envFiles := []string{string(envComponentPath)}
+
+	return expander.Expand(envFiles)
+}
diff --git a/cmd/root.go b/cmd/root.go
index 00b9572b..1906a62d 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -121,6 +121,10 @@ var RootCmd = &cobra.Command{
 // clientConfig.Namespace() is broken in client-go 3.0:
 // namespace in config erroneously overrides explicit --namespace
 func namespace() (string, error) {
+	return namespaceFor(clientConfig, overrides)
+}
+
+func namespaceFor(c clientcmd.ClientConfig, overrides clientcmd.ConfigOverrides) (string, error) {
 	if overrides.Context.Namespace != "" {
 		return overrides.Context.Namespace, nil
 	}
@@ -256,15 +260,15 @@ func dumpJSON(v interface{}) string {
 	return string(buf.Bytes())
 }
 
-func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
+func restClient(cmd *cobra.Command, envName *string, config clientcmd.ClientConfig, overrides clientcmd.ConfigOverrides) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
 	if envName != nil {
-		err := overrideCluster(*envName)
+		err := overrideCluster(*envName, config, overrides)
 		if err != nil {
 			return nil, nil, err
 		}
 	}
 
-	conf, err := clientConfig.ClientConfig()
+	conf, err := config.ClientConfig()
 	if err != nil {
 		return nil, nil, err
 	}
@@ -282,6 +286,10 @@ func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, di
 	return pool, discoCache, nil
 }
 
+func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, error) {
+	return restClient(cmd, envName, clientConfig, overrides)
+}
+
 type envSpec struct {
 	env   *string
 	files []string
@@ -317,9 +325,8 @@ func parseEnvCmd(cmd *cobra.Command, args []string) (*envSpec, error) {
 //
 // If the environment URI the user is attempting to deploy to is not the current
 // kubeconfig context, we must manually override the client-go --cluster flag
-// to ensure we are deploying to the correct cluster. The same logic applies
-// for overwriting the --namespace flag.
-func overrideCluster(envName string) error {
+// to ensure we are deploying to the correct cluster.
+func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overrides clientcmd.ConfigOverrides) error {
 	cwd, err := os.Getwd()
 	if err != nil {
 		return err
@@ -363,7 +370,7 @@ func overrideCluster(envName string) error {
 		return nil
 	}
 
-	return fmt.Errorf("Attempting to deploy to environment '%s' at %s, but there are no clusters with that URI", envName, env.URI)
+	return fmt.Errorf("Attempting to operate on environment '%s' at %s, but there are no clusters with that URI", envName, env.URI)
 }
 
 // expandEnvCmdObjs finds and expands templates for the family of commands of
@@ -405,8 +412,9 @@ func expandEnvCmdObjs(cmd *cobra.Command, envSpec *envSpec, cwd metadata.AbsPath
 			if err != nil {
 				return nil, err
 			}
-			baseObjExtCode := fmt.Sprintf("%s=%s", metadata.ComponentsExtCodeKey, constructBaseObj(componentPaths))
-			expander.ExtCodes = append([]string{baseObjExtCode}, expander.ExtCodes...)
+
+			baseObj := constructBaseObj(componentPaths)
+			expander.ExtCodes = append([]string{baseObj}, expander.ExtCodes...)
 			fileNames = []string{string(envComponentPath)}
 		}
 	}
@@ -437,5 +445,5 @@ func constructBaseObj(paths []string) string {
 		fmt.Fprintf(&obj, "  %s: import \"%s\",\n", name, p)
 	}
 	obj.WriteString("}\n")
-	return obj.String()
+	return fmt.Sprintf("%s=%s", metadata.ComponentsExtCodeKey, obj.String())
 }
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 3ee755b6..2bab864d 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -16,6 +16,7 @@
 package cmd
 
 import (
+	"fmt"
 	"testing"
 )
 
@@ -68,7 +69,7 @@ func TestConstructBaseObj(t *testing.T) {
 
 	for _, s := range tests {
 		res := constructBaseObj(s.inputPaths)
-		if res != s.expected {
+		if res != fmt.Sprintf("__ksonnet/components=%s", s.expected) {
 			t.Errorf("Wrong object constructed\n  expected: %v\n  got: %v", s.expected, res)
 		}
 	}
diff --git a/pkg/kubecfg/apply.go b/pkg/kubecfg/apply.go
index 4f39eedb..0fb8a659 100644
--- a/pkg/kubecfg/apply.go
+++ b/pkg/kubecfg/apply.go
@@ -13,7 +13,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/diff"
+	kdiff "k8s.io/apimachinery/pkg/util/diff"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/dynamic"
@@ -100,7 +100,7 @@ func (c ApplyCmd) Run(apiObjects []*unstructured.Unstructured, wd metadata.AbsPa
 			return fmt.Errorf("Error updating %s: %s", desc, err)
 		}
 
-		log.Debug("Updated object: ", diff.ObjectDiff(obj, newobj))
+		log.Debug("Updated object: ", kdiff.ObjectDiff(obj, newobj))
 
 		// Some objects appear under multiple kinds
 		// (eg: Deployment is both extensions/v1beta1
diff --git a/pkg/kubecfg/diff.go b/pkg/kubecfg/diff.go
index 59ebb2b8..e7b2c119 100644
--- a/pkg/kubecfg/diff.go
+++ b/pkg/kubecfg/diff.go
@@ -35,70 +35,200 @@ import (
 
 var ErrDiffFound = fmt.Errorf("Differences found.")
 
-// DiffCmd represents the diff subcommand
-type DiffCmd struct {
+// 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
+}
 
-	DiffStrategy string
+// 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 DiffCmd) Run(apiObjects []*unstructured.Unstructured, out io.Writer) error {
-	sort.Sort(utils.AlphabeticalOrder(apiObjects))
+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 _, obj := range apiObjects {
-		desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(c.Discovery, obj), utils.FqName(obj))
-		log.Debugf("Fetching ", desc)
+	for _, o := range a {
+		desc := hash(discovery, o, fqName)
+		var bObj map[string]interface{}
+		if b[desc] != nil {
+			bObj = b[desc].Object
+		}
 
-		client, err := utils.ClientForResource(c.ClientPool, c.Discovery, obj, c.Namespace)
+		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)
-			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
+		} else if err != nil {
+			return nil, nil, fmt.Errorf("Error fetching %s: %v", desc, err)
 		}
 
-		liveObjObject := liveObj.Object
-		if c.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)
-		}
+		liveObjs = append(liveObjs, liveObj)
+		liveObjsMap[desc] = liveObj
 	}
 
-	if diffFound {
-		return ErrDiffFound
-	}
-	return nil
+	return liveObjs, liveObjsMap, nil
 }
 
 func removeFields(config, live interface{}) interface{} {
diff --git a/utils/meta.go b/utils/meta.go
index 110aa769..41c82778 100644
--- a/utils/meta.go
+++ b/utils/meta.go
@@ -95,6 +95,12 @@ func ResourceNameFor(disco discovery.ServerResourcesInterface, o runtime.Object)
 	return strings.ToLower(gvk.Kind)
 }
 
+// GroupVersionKindFor returns a lowercased kind for an Kubernete's object
+func GroupVersionKindFor(o runtime.Object) string {
+	gvk := o.GetObjectKind().GroupVersionKind()
+	return strings.ToLower(gvk.Kind)
+}
+
 // FqName returns "namespace.name"
 func FqName(o metav1.Object) string {
 	if o.GetNamespace() == "" {
-- 
GitLab