From 4123a61df7d4898e65b423bcff30784f88bae83b Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 2 Dec 2015 12:20:10 -0800 Subject: [PATCH] Add the client side bits of kubectl export --- contrib/completions/bash/kubectl | 1 + docs/man/man1/kubectl-get.1 | 4 ++++ docs/user-guide/kubectl/kubectl_get.md | 3 ++- hack/lib/test.sh | 3 ++- hack/test-cmd.sh | 3 +++ pkg/kubectl/cmd/apply_test.go | 1 + pkg/kubectl/cmd/cmd_test.go | 6 ++++++ pkg/kubectl/cmd/create_test.go | 6 ++++++ pkg/kubectl/cmd/get.go | 4 ++++ pkg/kubectl/resource/builder.go | 14 +++++++++++--- pkg/kubectl/resource/helper.go | 26 ++++++++++++++++---------- pkg/kubectl/resource/helper_test.go | 4 ++-- pkg/kubectl/resource/selector.go | 6 ++++-- pkg/kubectl/resource/visitor.go | 11 +++++++---- 14 files changed, 69 insertions(+), 23 deletions(-) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index d2448b23544..e7380383f05 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -265,6 +265,7 @@ _kubectl_get() flags_completion=() flags+=("--all-namespaces") + flags+=("--export") flags+=("--filename=") flags_with_completion+=("--filename") flags_completion+=("__handle_filename_extension_flag json|yaml|yml") diff --git a/docs/man/man1/kubectl-get.1 b/docs/man/man1/kubectl-get.1 index c1d3d91c1f2..0c517589e5a 100644 --- a/docs/man/man1/kubectl-get.1 +++ b/docs/man/man1/kubectl-get.1 @@ -32,6 +32,10 @@ of the \-\-template flag, you can filter the attributes of the fetched resource( \fB\-\-all\-namespaces\fP=false If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with \-\-namespace. +.PP +\fB\-\-export\fP=false + If true, use 'export' for the resources. Exported resources are stripped of cluster\-specific information. + .PP \fB\-f\fP, \fB\-\-filename\fP=[] Filename, directory, or URL to a file identifying the resource to get from a server. diff --git a/docs/user-guide/kubectl/kubectl_get.md b/docs/user-guide/kubectl/kubectl_get.md index ebe672f810b..25f21b8720f 100644 --- a/docs/user-guide/kubectl/kubectl_get.md +++ b/docs/user-guide/kubectl/kubectl_get.md @@ -86,6 +86,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 ``` --all-namespaces[=false]: If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + --export[=false]: If true, use 'export' for the resources. Exported resources are stripped of cluster-specific information. -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to get from a server. -L, --label-columns=[]: Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag statements like -L label1 -L label2... --no-headers[=false]: When using the default output, don't print headers. @@ -131,7 +132,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra on 8-Dec-2015 +###### Auto generated by spf13/cobra on 22-Dec-2015 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_get.md?pixel)]() diff --git a/hack/lib/test.sh b/hack/lib/test.sh index 4a34f2ea902..99c5847f923 100644 --- a/hack/lib/test.sh +++ b/hack/lib/test.sh @@ -30,8 +30,9 @@ kube::test::get_object_assert() { local object=$1 local request=$2 local expected=$3 + local args=${4:-} - res=$(eval kubectl get "${kube_flags[@]}" $object -o go-template=\"$request\") + res=$(eval kubectl ${args} get "${kube_flags[@]}" $object -o go-template=\"$request\") if [[ "$res" =~ ^$expected$ ]]; then echo -n ${green} diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 038a0eae70d..84bd2fd19b0 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -287,6 +287,9 @@ runTests() { # Describe command (resource only) should print detailed information kube::test::describe_resource_assert pods "Name:" "Image(s):" "Node:" "Labels:" "Status:" "Controllers" + ### Validate Export ### + kube::test::get_object_assert 'pods/valid-pod' "{{.metadata.namespace}} {{.metadata.name}}" ' valid-pod' "--export=true" + ### Dump current valid-pod POD output_pod=$(kubectl get pod valid-pod -o yaml --output-version=v1 "${kube_flags[@]}") diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go index 8773d22cfc0..bea70408ce6 100644 --- a/pkg/kubectl/cmd/apply_test.go +++ b/pkg/kubectl/cmd/apply_test.go @@ -170,6 +170,7 @@ func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[ } func TestApplyObject(t *testing.T) { + initTestErrorHandler(t) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 3b5b50dac81..189b402f6f4 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -41,6 +41,12 @@ import ( "k8s.io/kubernetes/pkg/util" ) +func initTestErrorHandler(t *testing.T) { + cmdutil.BehaviorOnFatal(func(str string) { + t.Errorf("Error running command: %s", str) + }) +} + type internalType struct { Kind string APIVersion string diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index eaa9408bdd7..9fdee69f1a2 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -27,6 +27,7 @@ import ( ) func TestExtraArgsFail(t *testing.T) { + initTestErrorHandler(t) buf := bytes.NewBuffer([]byte{}) f, _, _ := NewAPIFactory() @@ -37,6 +38,7 @@ func TestExtraArgsFail(t *testing.T) { } func TestCreateObject(t *testing.T) { + initTestErrorHandler(t) _, _, rc := testData() rc.Items[0].Name = "redis-master-controller" @@ -69,6 +71,7 @@ func TestCreateObject(t *testing.T) { } func TestCreateMultipleObject(t *testing.T) { + initTestErrorHandler(t) _, svc, rc := testData() f, tf, codec := NewAPIFactory() @@ -103,6 +106,7 @@ func TestCreateMultipleObject(t *testing.T) { } func TestCreateDirectory(t *testing.T) { + initTestErrorHandler(t) _, svc, rc := testData() rc.Items[0].Name = "name" @@ -136,6 +140,7 @@ func TestCreateDirectory(t *testing.T) { } func TestPrintObjectSpecificMessage(t *testing.T) { + initTestErrorHandler(t) tests := []struct { obj runtime.Object expectOutput bool @@ -170,6 +175,7 @@ func TestPrintObjectSpecificMessage(t *testing.T) { } func TestMakePortsString(t *testing.T) { + initTestErrorHandler(t) tests := []struct { ports []api.ServicePort useNodePort bool diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 509c45190ff..e4c3f009b2e 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -101,6 +101,7 @@ func NewCmdGet(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().Bool("watch-only", false, "Watch for changes to the requested object(s), without listing/getting first.") cmd.Flags().Bool("all-namespaces", false, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().StringSliceP("label-columns", "L", []string{}, "Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag statements like -L label1 -L label2...") + cmd.Flags().Bool("export", false, "If true, use 'export' for the resources. Exported resources are stripped of cluster-specific information.") usage := "Filename, directory, or URL to a file identifying the resource to get from a server." kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) return cmd @@ -131,6 +132,7 @@ func RunGet(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string if len(options.Filenames) > 0 || argsHasNames { cmd.Flag("show-all").Value.Set("true") } + export := cmdutil.GetFlagBool(cmd, "export") // handle watch separately since we cannot watch multiple resource types isWatch, isWatchOnly := cmdutil.GetFlagBool(cmd, "watch"), cmdutil.GetFlagBool(cmd, "watch-only") @@ -139,6 +141,7 @@ func RunGet(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces). FilenameParam(enforceNamespace, options.Filenames...). SelectorParam(selector). + ExportParam(export). ResourceTypeOrNameArgs(true, args...). SingleResourceType(). Latest(). @@ -193,6 +196,7 @@ func RunGet(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces). FilenameParam(enforceNamespace, options.Filenames...). SelectorParam(selector). + ExportParam(export). ResourceTypeOrNameArgs(true, args...). ContinueOnError(). Latest() diff --git a/pkg/kubectl/resource/builder.go b/pkg/kubectl/resource/builder.go index bba7a781890..dc6c54e4157 100644 --- a/pkg/kubectl/resource/builder.go +++ b/pkg/kubectl/resource/builder.go @@ -68,6 +68,8 @@ type Builder struct { singleResourceType bool continueOnError bool + export bool + schema validation.Schema } @@ -234,6 +236,12 @@ func (b *Builder) Selector(selector labels.Selector) *Builder { return b } +// ExportParam accepts the export boolean for these resources +func (b *Builder) ExportParam(export bool) *Builder { + b.export = export + return b +} + // NamespaceParam accepts the namespace that these resources should be // considered under from - used by DefaultNamespace() and RequireNamespace() func (b *Builder) NamespaceParam(namespace string) *Builder { @@ -512,7 +520,7 @@ func (b *Builder) visitorResult() *Result { if mapping.Scope.Name() != meta.RESTScopeNameNamespace { selectorNamespace = "" } - visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, b.selector)) + visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, b.selector, b.export)) } if b.continueOnError { return &Result{visitor: EagerVisitorList(visitors), sources: visitors} @@ -570,7 +578,7 @@ func (b *Builder) visitorResult() *Result { } } - info := NewInfo(client, mapping, selectorNamespace, tuple.Name) + info := NewInfo(client, mapping, selectorNamespace, tuple.Name, b.export) items = append(items, info) } @@ -619,7 +627,7 @@ func (b *Builder) visitorResult() *Result { visitors := []Visitor{} for _, name := range b.names { - info := NewInfo(client, mapping, selectorNamespace, name) + info := NewInfo(client, mapping, selectorNamespace, name, b.export) visitors = append(visitors, info) } return &Result{singular: isSingular, visitor: VisitorList(visitors), sources: visitors} diff --git a/pkg/kubectl/resource/helper.go b/pkg/kubectl/resource/helper.go index 9ab434a09d6..85f7bc1946c 100644 --- a/pkg/kubectl/resource/helper.go +++ b/pkg/kubectl/resource/helper.go @@ -17,6 +17,8 @@ limitations under the License. package resource import ( + "strconv" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/labels" @@ -51,23 +53,27 @@ func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper { } } -func (m *Helper) Get(namespace, name string) (runtime.Object, error) { - return m.RESTClient.Get(). +func (m *Helper) Get(namespace, name string, export bool) (runtime.Object, error) { + req := m.RESTClient.Get(). NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). - Name(name). - Do(). - Get() + Name(name) + if export { + req.Param("export", strconv.FormatBool(export)) + } + return req.Do().Get() } // TODO: add field selector -func (m *Helper) List(namespace, apiVersion string, selector labels.Selector) (runtime.Object, error) { - return m.RESTClient.Get(). +func (m *Helper) List(namespace, apiVersion string, selector labels.Selector, export bool) (runtime.Object, error) { + req := m.RESTClient.Get(). NamespaceIfScoped(namespace, m.NamespaceScoped). Resource(m.Resource). - LabelsSelectorParam(selector). - Do(). - Get() + LabelsSelectorParam(selector) + if export { + req.Param("export", strconv.FormatBool(export)) + } + return req.Do().Get() } func (m *Helper) Watch(namespace, resourceVersion, apiVersion string, labelSelector labels.Selector) (watch.Interface, error) { diff --git a/pkg/kubectl/resource/helper_test.go b/pkg/kubectl/resource/helper_test.go index 7d7007236a0..2bdc977b0ff 100644 --- a/pkg/kubectl/resource/helper_test.go +++ b/pkg/kubectl/resource/helper_test.go @@ -270,7 +270,7 @@ func TestHelperGet(t *testing.T) { RESTClient: client, NamespaceScoped: true, } - obj, err := modifier.Get("bar", "foo") + obj, err := modifier.Get("bar", "foo", false) if (err != nil) != test.Err { t.Errorf("unexpected error: %t %v", test.Err, err) } @@ -341,7 +341,7 @@ func TestHelperList(t *testing.T) { RESTClient: client, NamespaceScoped: true, } - obj, err := modifier.List("bar", testapi.Default.GroupVersion().String(), labels.SelectorFromSet(labels.Set{"foo": "baz"})) + obj, err := modifier.List("bar", testapi.Default.GroupVersion().String(), labels.SelectorFromSet(labels.Set{"foo": "baz"}), false) if (err != nil) != test.Err { t.Errorf("unexpected error: %t %v", test.Err, err) } diff --git a/pkg/kubectl/resource/selector.go b/pkg/kubectl/resource/selector.go index 4bca8d9fd50..047a814ae35 100644 --- a/pkg/kubectl/resource/selector.go +++ b/pkg/kubectl/resource/selector.go @@ -31,21 +31,23 @@ type Selector struct { Mapping *meta.RESTMapping Namespace string Selector labels.Selector + Export bool } // NewSelector creates a resource selector which hides details of getting items by their label selector. -func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace string, selector labels.Selector) *Selector { +func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace string, selector labels.Selector, export bool) *Selector { return &Selector{ Client: client, Mapping: mapping, Namespace: namespace, Selector: selector, + Export: export, } } // Visit implements Visitor func (r *Selector) Visit(fn VisitorFunc) error { - list, err := NewHelper(r.Client, r.Mapping).List(r.Namespace, r.ResourceMapping().GroupVersionKind.GroupVersion().String(), r.Selector) + list, err := NewHelper(r.Client, r.Mapping).List(r.Namespace, r.ResourceMapping().GroupVersionKind.GroupVersion().String(), r.Selector, r.Export) if err != nil { if errors.IsBadRequest(err) || errors.IsNotFound(err) { if r.Selector.Empty() { diff --git a/pkg/kubectl/resource/visitor.go b/pkg/kubectl/resource/visitor.go index 29e17114998..5054a723852 100644 --- a/pkg/kubectl/resource/visitor.go +++ b/pkg/kubectl/resource/visitor.go @@ -85,15 +85,18 @@ type Info struct { // but if set it should be equal to or newer than the resource version of the // object (however the server defines resource version). ResourceVersion string + // Optional, should this resource be exported, stripped of cluster-specific and instance specific fields + Export bool } // NewInfo returns a new info object -func NewInfo(client RESTClient, mapping *meta.RESTMapping, namespace, name string) *Info { +func NewInfo(client RESTClient, mapping *meta.RESTMapping, namespace, name string, export bool) *Info { return &Info{ Client: client, Mapping: mapping, Namespace: namespace, Name: name, + Export: export, } } @@ -103,8 +106,8 @@ func (i *Info) Visit(fn VisitorFunc) error { } // Get retrieves the object from the Namespace and Name fields -func (i *Info) Get() error { - obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name) +func (i *Info) Get() (err error) { + obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name, i.Export) if err != nil { return err } @@ -573,7 +576,7 @@ func RetrieveLatest(info *Info, err error) error { if info.Namespaced() && len(info.Namespace) == 0 { return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name) } - obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name) + obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name, info.Export) if err != nil { return err }