diff --git a/cmd/diff.go b/cmd/diff.go index f719bfb119630cd0c0b63f3b41beac78771d39ff..bddf771ec2d81156809aac52ea7fb3215dac71d2 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" @@ -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) +} diff --git a/cmd/root.go b/cmd/root.go index 7cea5a3c56bbf4b781b74a1e7dd85c4d4deb6c83..e738adc3a93f7cc60d3bcc56d0e7608cf6501443 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 @@ -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()) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 3ee755b64e9abaaccd6181f13499570b2fecdaab..2bab864d54004a5615573529b4335a3fb993488e 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 4f39eedb59629020ffa9d1fa1b4917e94191da2d..0fb8a6596ddf63843ed68007b8b1f9b97c0a429a 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 59ebb2b8bec7ee5eae1e430d8a6169b2c38c6960..e7b2c119fb5bdf7904089a770cdefceea5723e60 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 110aa7693fa5249178d6ce45fd3045a13f88c169..41c827781db14d716745b1163943f5f05c425d80 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() == "" {