Skip to content
Snippets Groups Projects
Commit b70c996f authored by Alex Clemmer's avatar Alex Clemmer Committed by GitHub
Browse files

Merge pull request #19 from jessicayuen/env-diff

Support `diff`ing between two environments
parents c9ae99d2 2687c6d8
No related branches found
No related tags found
No related merge requests found
......@@ -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"
......@@ -29,29 +35,20 @@ const flagDiffStrategy = "diff-strategy"
func init() {
addEnvCmdFlags(diffCmd)
bindClientGoFlags(diffCmd)
bindJsonnetFlags(diffCmd)
diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
RootCmd.AddCommand(diffCmd)
}
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 {
......@@ -59,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'.
......@@ -102,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)
}
......@@ -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
......@@ -361,7 +368,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
......@@ -403,8 +410,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)}
}
}
......@@ -435,5 +443,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())
}
......@@ -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)
}
}
......
......@@ -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
......
......@@ -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{} {
......
......@@ -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() == "" {
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment