From a510285d635e77bf88d6889f15b0d8d4c0132131 Mon Sep 17 00:00:00 2001 From: juanvallejo Date: Wed, 22 Aug 2018 19:59:23 -0400 Subject: [PATCH] add support for --cluster --context --user flags --- .../sample-cli-plugin/cmd/kubectl-ns.go | 16 ++ .../k8s.io/sample-cli-plugin/pkg/cmd/ns.go | 210 +++++++++++++----- 2 files changed, 170 insertions(+), 56 deletions(-) diff --git a/staging/src/k8s.io/sample-cli-plugin/cmd/kubectl-ns.go b/staging/src/k8s.io/sample-cli-plugin/cmd/kubectl-ns.go index 5bf5b361c39..2b5fbb07e55 100644 --- a/staging/src/k8s.io/sample-cli-plugin/cmd/kubectl-ns.go +++ b/staging/src/k8s.io/sample-cli-plugin/cmd/kubectl-ns.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 The Kubernetes 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 main import ( diff --git a/staging/src/k8s.io/sample-cli-plugin/pkg/cmd/ns.go b/staging/src/k8s.io/sample-cli-plugin/pkg/cmd/ns.go index 3a1ae2f1303..13f26cce451 100644 --- a/staging/src/k8s.io/sample-cli-plugin/pkg/cmd/ns.go +++ b/staging/src/k8s.io/sample-cli-plugin/pkg/cmd/ns.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 The Kubernetes 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 cmd import ( @@ -13,21 +29,33 @@ import ( ) var ( - namespace_example = ` + namespaceExample = ` # view the current namespace in your KUBECONFIG %[1]s ns # view all of the namespaces in use by contexts in your KUBECONFIG - %[1] ns --list + %[1]s ns --list # switch your current-context to one that contains the desired namespace %[1]s ns foo ` + + errNoContext = fmt.Errorf("no context is currently set, use %q to select a new one", "kubectl config use-context ") ) +// NamespaceOptions provides information required to update +// the current context on a user's KUBECONFIG type NamespaceOptions struct { configFlags *genericclioptions.ConfigFlags + resultingContext *api.Context + resultingContextName string + + userSpecifiedCluster string + userSpecifiedContext string + userSpecifiedAuthInfo string + userSpecifiedNamespace string + rawConfig api.Config listNamespaces bool args []string @@ -35,6 +63,7 @@ type NamespaceOptions struct { genericclioptions.IOStreams } +// NewNamespaceOptions provides an instance of NamespaceOptions with default values func NewNamespaceOptions(streams genericclioptions.IOStreams) *NamespaceOptions { return &NamespaceOptions{ configFlags: genericclioptions.NewConfigFlags(), @@ -43,16 +72,17 @@ func NewNamespaceOptions(streams genericclioptions.IOStreams) *NamespaceOptions } } +// NewCmdNamespace provides a cobra command wrapping NamespaceOptions func NewCmdNamespace(streams genericclioptions.IOStreams) *cobra.Command { o := NewNamespaceOptions(streams) cmd := &cobra.Command{ Use: "ns [new-namespace] [flags]", Short: "View or set the current namespace", - Example: namespace_example, + Example: fmt.Sprintf(namespaceExample, "kubectl"), SilenceUsage: true, RunE: func(c *cobra.Command, args []string) error { - if err := o.Complete(args); err != nil { + if err := o.Complete(c, args); err != nil { return err } if err := o.Validate(); err != nil { @@ -72,20 +102,103 @@ func NewCmdNamespace(streams genericclioptions.IOStreams) *cobra.Command { return cmd } -func (o *NamespaceOptions) Complete(args []string) error { +// Complete sets all information required for updating the current context +func (o *NamespaceOptions) Complete(cmd *cobra.Command, args []string) error { + o.args = args + var err error o.rawConfig, err = o.configFlags.ToRawKubeConfigLoader().RawConfig() if err != nil { return err } - o.args = args + o.userSpecifiedNamespace, err = cmd.Flags().GetString("namespace") + if err != nil { + return err + } + if len(args) > 0 { + if len(o.userSpecifiedNamespace) > 0 { + return fmt.Errorf("cannot specify both a --namespace value and a new namespace argument") + } + + o.userSpecifiedNamespace = args[0] + } + + // if no namespace argument or flag value was specified, then there + // is no need to generate a resulting context + if len(o.userSpecifiedNamespace) == 0 { + return nil + } + + o.userSpecifiedContext, err = cmd.Flags().GetString("context") + if err != nil { + return err + } + + o.userSpecifiedCluster, err = cmd.Flags().GetString("cluster") + if err != nil { + return err + } + + o.userSpecifiedAuthInfo, err = cmd.Flags().GetString("user") + if err != nil { + return err + } + + currentContext, exists := o.rawConfig.Contexts[o.rawConfig.CurrentContext] + if !exists { + return errNoContext + } + + o.resultingContext = api.NewContext() + o.resultingContext.Cluster = currentContext.Cluster + o.resultingContext.AuthInfo = currentContext.AuthInfo + + // if a target context is explicitly provided by the user, + // use that as our reference for the final, resulting context + if len(o.userSpecifiedContext) > 0 { + o.resultingContextName = o.userSpecifiedContext + if userCtx, exists := o.rawConfig.Contexts[o.userSpecifiedContext]; exists { + o.resultingContext = userCtx.DeepCopy() + } + } + + // override context info with user provided values + o.resultingContext.Namespace = o.userSpecifiedNamespace + + if len(o.userSpecifiedCluster) > 0 { + o.resultingContext.Cluster = o.userSpecifiedCluster + } + if len(o.userSpecifiedAuthInfo) > 0 { + o.resultingContext.AuthInfo = o.userSpecifiedAuthInfo + } + + // generate a unique context name based on its new values if + // user did not explicitly request a context by name + if len(o.userSpecifiedContext) == 0 { + o.resultingContextName = generateContextName(o.resultingContext) + } + return nil } +func generateContextName(fromContext *api.Context) string { + name := fromContext.Namespace + if len(fromContext.Cluster) > 0 { + name = fmt.Sprintf("%s/%s", name, fromContext.Cluster) + } + if len(fromContext.AuthInfo) > 0 { + cleanAuthInfo := strings.Split(fromContext.AuthInfo, "/")[0] + name = fmt.Sprintf("%s/%s", name, cleanAuthInfo) + } + + return name +} + +// Validate ensures that all required arguments and flag values are provided func (o *NamespaceOptions) Validate() error { if len(o.rawConfig.CurrentContext) == 0 { - return fmt.Errorf("no context is currently set in your configuration") + return errNoContext } if len(o.args) > 1 { return fmt.Errorf("either one or no arguments are allowed") @@ -94,9 +207,11 @@ func (o *NamespaceOptions) Validate() error { return nil } +// Run lists all available namespaces on a user's KUBECONFIG or updates the +// current context based on a provided namespace. func (o *NamespaceOptions) Run() error { - if len(o.args) > 0 && len(o.args[0]) > 0 { - return o.setNamespace(o.args[0]) + if len(o.userSpecifiedNamespace) > 0 && o.resultingContext != nil { + return o.setNamespace(o.resultingContext, o.resultingContextName) } namespaces := map[string]bool{} @@ -134,58 +249,41 @@ func (o *NamespaceOptions) Run() error { return nil } -func (o *NamespaceOptions) setNamespace(newNamespace string) error { - if len(newNamespace) == 0 { +func isContextEqual(ctxA, ctxB *api.Context) bool { + if ctxA == nil || ctxB == nil { + return false + } + if ctxA.Cluster != ctxB.Cluster { + return false + } + if ctxA.Namespace != ctxB.Namespace { + return false + } + if ctxA.AuthInfo != ctxB.AuthInfo { + return false + } + + return true +} + +// setNamespace receives a "desired" context state and determines if a similar context +// is already present in a user's KUBECONFIG. If one is not, then a new context is added +// to the user's config under the provided destination name. +// The current context field is updated to point to the new context. +func (o *NamespaceOptions) setNamespace(fromContext *api.Context, withContextName string) error { + if len(fromContext.Namespace) == 0 { return fmt.Errorf("a non-empty namespace must be provided") } - existingCtx, ok := o.rawConfig.Contexts[o.rawConfig.CurrentContext] - if !ok { - return fmt.Errorf("unable to gather information about the current context") - } - - if existingCtx.Namespace == newNamespace { - fmt.Fprintf(o.Out, "already using namespace %q\n", newNamespace) - return nil - } - - // determine if a context exists for the new namespace - existingCtxName := "" - for name, c := range o.rawConfig.Contexts { - if c.Namespace != newNamespace || c.Cluster != existingCtx.Cluster || c.AuthInfo != existingCtx.AuthInfo { - continue - } - - existingCtxName = name - break - } - - if len(existingCtxName) == 0 { - newCtx := api.NewContext() - newCtx.AuthInfo = existingCtx.AuthInfo - newCtx.Cluster = existingCtx.Cluster - newCtx.Namespace = newNamespace - - newCtxName := newNamespace - if len(existingCtx.Cluster) > 0 { - newCtxName = fmt.Sprintf("%s/%s", newCtxName, existingCtx.Cluster) - } - if len(existingCtx.AuthInfo) > 0 { - cleanAuthInfo := strings.Split(existingCtx.AuthInfo, "/")[0] - newCtxName = fmt.Sprintf("%s/%s", newCtxName, cleanAuthInfo) - } - - o.rawConfig.Contexts[newCtxName] = newCtx - existingCtxName = newCtxName - } - configAccess := clientcmd.NewDefaultPathOptions() - o.rawConfig.CurrentContext = existingCtxName - if err := clientcmd.ModifyConfig(configAccess, o.rawConfig, true); err != nil { - return err + // determine if we have already saved this context to the user's KUBECONFIG before + // if so, simply switch the current context to the existing one. + if existingResultingCtx, exists := o.rawConfig.Contexts[withContextName]; !exists || !isContextEqual(fromContext, existingResultingCtx) { + o.rawConfig.Contexts[withContextName] = fromContext } + o.rawConfig.CurrentContext = withContextName - fmt.Fprintf(o.Out, "namespace changed to %q\n", newNamespace) - return nil + fmt.Fprintf(o.Out, "namespace changed to %q\n", fromContext.Namespace) + return clientcmd.ModifyConfig(configAccess, o.rawConfig, true) }