Add the client side bits of kubectl export

This commit is contained in:
Brendan Burns 2015-12-02 12:20:10 -08:00
parent 4ca66d2aef
commit 4123a61df7
14 changed files with 69 additions and 23 deletions

View File

@ -265,6 +265,7 @@ _kubectl_get()
flags_completion=() flags_completion=()
flags+=("--all-namespaces") flags+=("--all-namespaces")
flags+=("--export")
flags+=("--filename=") flags+=("--filename=")
flags_with_completion+=("--filename") flags_with_completion+=("--filename")
flags_completion+=("__handle_filename_extension_flag json|yaml|yml") flags_completion+=("__handle_filename_extension_flag json|yaml|yml")

View File

@ -32,6 +32,10 @@ of the \-\-template flag, you can filter the attributes of the fetched resource(
\fB\-\-all\-namespaces\fP=false \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. 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 .PP
\fB\-f\fP, \fB\-\-filename\fP=[] \fB\-f\fP, \fB\-\-filename\fP=[]
Filename, directory, or URL to a file identifying the resource to get from a server. Filename, directory, or URL to a file identifying the resource to get from a server.

View File

@ -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. --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. -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... -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. --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 * [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
<!-- BEGIN MUNGE: GENERATED_ANALYTICS --> <!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_get.md?pixel)]() [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_get.md?pixel)]()

View File

@ -30,8 +30,9 @@ kube::test::get_object_assert() {
local object=$1 local object=$1
local request=$2 local request=$2
local expected=$3 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 if [[ "$res" =~ ^$expected$ ]]; then
echo -n ${green} echo -n ${green}

View File

@ -287,6 +287,9 @@ runTests() {
# Describe command (resource only) should print detailed information # Describe command (resource only) should print detailed information
kube::test::describe_resource_assert pods "Name:" "Image(s):" "Node:" "Labels:" "Status:" "Controllers" 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}}" '<no value> valid-pod' "--export=true"
### Dump current valid-pod POD ### Dump current valid-pod POD
output_pod=$(kubectl get pod valid-pod -o yaml --output-version=v1 "${kube_flags[@]}") output_pod=$(kubectl get pod valid-pod -o yaml --output-version=v1 "${kube_flags[@]}")

View File

@ -170,6 +170,7 @@ func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[
} }
func TestApplyObject(t *testing.T) { func TestApplyObject(t *testing.T) {
initTestErrorHandler(t)
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRC := "/namespaces/test/replicationcontrollers/" + nameRC

View File

@ -41,6 +41,12 @@ import (
"k8s.io/kubernetes/pkg/util" "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 { type internalType struct {
Kind string Kind string
APIVersion string APIVersion string

View File

@ -27,6 +27,7 @@ import (
) )
func TestExtraArgsFail(t *testing.T) { func TestExtraArgsFail(t *testing.T) {
initTestErrorHandler(t)
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
f, _, _ := NewAPIFactory() f, _, _ := NewAPIFactory()
@ -37,6 +38,7 @@ func TestExtraArgsFail(t *testing.T) {
} }
func TestCreateObject(t *testing.T) { func TestCreateObject(t *testing.T) {
initTestErrorHandler(t)
_, _, rc := testData() _, _, rc := testData()
rc.Items[0].Name = "redis-master-controller" rc.Items[0].Name = "redis-master-controller"
@ -69,6 +71,7 @@ func TestCreateObject(t *testing.T) {
} }
func TestCreateMultipleObject(t *testing.T) { func TestCreateMultipleObject(t *testing.T) {
initTestErrorHandler(t)
_, svc, rc := testData() _, svc, rc := testData()
f, tf, codec := NewAPIFactory() f, tf, codec := NewAPIFactory()
@ -103,6 +106,7 @@ func TestCreateMultipleObject(t *testing.T) {
} }
func TestCreateDirectory(t *testing.T) { func TestCreateDirectory(t *testing.T) {
initTestErrorHandler(t)
_, svc, rc := testData() _, svc, rc := testData()
rc.Items[0].Name = "name" rc.Items[0].Name = "name"
@ -136,6 +140,7 @@ func TestCreateDirectory(t *testing.T) {
} }
func TestPrintObjectSpecificMessage(t *testing.T) { func TestPrintObjectSpecificMessage(t *testing.T) {
initTestErrorHandler(t)
tests := []struct { tests := []struct {
obj runtime.Object obj runtime.Object
expectOutput bool expectOutput bool
@ -170,6 +175,7 @@ func TestPrintObjectSpecificMessage(t *testing.T) {
} }
func TestMakePortsString(t *testing.T) { func TestMakePortsString(t *testing.T) {
initTestErrorHandler(t)
tests := []struct { tests := []struct {
ports []api.ServicePort ports []api.ServicePort
useNodePort bool useNodePort bool

View File

@ -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("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().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().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." usage := "Filename, directory, or URL to a file identifying the resource to get from a server."
kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage)
return cmd 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 { if len(options.Filenames) > 0 || argsHasNames {
cmd.Flag("show-all").Value.Set("true") cmd.Flag("show-all").Value.Set("true")
} }
export := cmdutil.GetFlagBool(cmd, "export")
// handle watch separately since we cannot watch multiple resource types // handle watch separately since we cannot watch multiple resource types
isWatch, isWatchOnly := cmdutil.GetFlagBool(cmd, "watch"), cmdutil.GetFlagBool(cmd, "watch-only") 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). NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces).
FilenameParam(enforceNamespace, options.Filenames...). FilenameParam(enforceNamespace, options.Filenames...).
SelectorParam(selector). SelectorParam(selector).
ExportParam(export).
ResourceTypeOrNameArgs(true, args...). ResourceTypeOrNameArgs(true, args...).
SingleResourceType(). SingleResourceType().
Latest(). Latest().
@ -193,6 +196,7 @@ func RunGet(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string
NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces). NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces).
FilenameParam(enforceNamespace, options.Filenames...). FilenameParam(enforceNamespace, options.Filenames...).
SelectorParam(selector). SelectorParam(selector).
ExportParam(export).
ResourceTypeOrNameArgs(true, args...). ResourceTypeOrNameArgs(true, args...).
ContinueOnError(). ContinueOnError().
Latest() Latest()

View File

@ -68,6 +68,8 @@ type Builder struct {
singleResourceType bool singleResourceType bool
continueOnError bool continueOnError bool
export bool
schema validation.Schema schema validation.Schema
} }
@ -234,6 +236,12 @@ func (b *Builder) Selector(selector labels.Selector) *Builder {
return b 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 // NamespaceParam accepts the namespace that these resources should be
// considered under from - used by DefaultNamespace() and RequireNamespace() // considered under from - used by DefaultNamespace() and RequireNamespace()
func (b *Builder) NamespaceParam(namespace string) *Builder { func (b *Builder) NamespaceParam(namespace string) *Builder {
@ -512,7 +520,7 @@ func (b *Builder) visitorResult() *Result {
if mapping.Scope.Name() != meta.RESTScopeNameNamespace { if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
selectorNamespace = "" selectorNamespace = ""
} }
visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, b.selector)) visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, b.selector, b.export))
} }
if b.continueOnError { if b.continueOnError {
return &Result{visitor: EagerVisitorList(visitors), sources: visitors} 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) items = append(items, info)
} }
@ -619,7 +627,7 @@ func (b *Builder) visitorResult() *Result {
visitors := []Visitor{} visitors := []Visitor{}
for _, name := range b.names { for _, name := range b.names {
info := NewInfo(client, mapping, selectorNamespace, name) info := NewInfo(client, mapping, selectorNamespace, name, b.export)
visitors = append(visitors, info) visitors = append(visitors, info)
} }
return &Result{singular: isSingular, visitor: VisitorList(visitors), sources: visitors} return &Result{singular: isSingular, visitor: VisitorList(visitors), sources: visitors}

View File

@ -17,6 +17,8 @@ limitations under the License.
package resource package resource
import ( import (
"strconv"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/labels" "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) { func (m *Helper) Get(namespace, name string, export bool) (runtime.Object, error) {
return m.RESTClient.Get(). req := m.RESTClient.Get().
NamespaceIfScoped(namespace, m.NamespaceScoped). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
Name(name). Name(name)
Do(). if export {
Get() req.Param("export", strconv.FormatBool(export))
}
return req.Do().Get()
} }
// TODO: add field selector // TODO: add field selector
func (m *Helper) List(namespace, apiVersion string, selector labels.Selector) (runtime.Object, error) { func (m *Helper) List(namespace, apiVersion string, selector labels.Selector, export bool) (runtime.Object, error) {
return m.RESTClient.Get(). req := m.RESTClient.Get().
NamespaceIfScoped(namespace, m.NamespaceScoped). NamespaceIfScoped(namespace, m.NamespaceScoped).
Resource(m.Resource). Resource(m.Resource).
LabelsSelectorParam(selector). LabelsSelectorParam(selector)
Do(). if export {
Get() req.Param("export", strconv.FormatBool(export))
}
return req.Do().Get()
} }
func (m *Helper) Watch(namespace, resourceVersion, apiVersion string, labelSelector labels.Selector) (watch.Interface, error) { func (m *Helper) Watch(namespace, resourceVersion, apiVersion string, labelSelector labels.Selector) (watch.Interface, error) {

View File

@ -270,7 +270,7 @@ func TestHelperGet(t *testing.T) {
RESTClient: client, RESTClient: client,
NamespaceScoped: true, NamespaceScoped: true,
} }
obj, err := modifier.Get("bar", "foo") obj, err := modifier.Get("bar", "foo", false)
if (err != nil) != test.Err { if (err != nil) != test.Err {
t.Errorf("unexpected error: %t %v", test.Err, err) t.Errorf("unexpected error: %t %v", test.Err, err)
} }
@ -341,7 +341,7 @@ func TestHelperList(t *testing.T) {
RESTClient: client, RESTClient: client,
NamespaceScoped: true, 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 { if (err != nil) != test.Err {
t.Errorf("unexpected error: %t %v", test.Err, err) t.Errorf("unexpected error: %t %v", test.Err, err)
} }

View File

@ -31,21 +31,23 @@ type Selector struct {
Mapping *meta.RESTMapping Mapping *meta.RESTMapping
Namespace string Namespace string
Selector labels.Selector Selector labels.Selector
Export bool
} }
// NewSelector creates a resource selector which hides details of getting items by their label selector. // 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{ return &Selector{
Client: client, Client: client,
Mapping: mapping, Mapping: mapping,
Namespace: namespace, Namespace: namespace,
Selector: selector, Selector: selector,
Export: export,
} }
} }
// Visit implements Visitor // Visit implements Visitor
func (r *Selector) Visit(fn VisitorFunc) error { 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 err != nil {
if errors.IsBadRequest(err) || errors.IsNotFound(err) { if errors.IsBadRequest(err) || errors.IsNotFound(err) {
if r.Selector.Empty() { if r.Selector.Empty() {

View File

@ -85,15 +85,18 @@ type Info struct {
// but if set it should be equal to or newer than the resource version of the // but if set it should be equal to or newer than the resource version of the
// object (however the server defines resource version). // object (however the server defines resource version).
ResourceVersion string ResourceVersion string
// Optional, should this resource be exported, stripped of cluster-specific and instance specific fields
Export bool
} }
// NewInfo returns a new info object // 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{ return &Info{
Client: client, Client: client,
Mapping: mapping, Mapping: mapping,
Namespace: namespace, Namespace: namespace,
Name: name, 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 // Get retrieves the object from the Namespace and Name fields
func (i *Info) Get() error { func (i *Info) Get() (err error) {
obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name) obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name, i.Export)
if err != nil { if err != nil {
return err return err
} }
@ -573,7 +576,7 @@ func RetrieveLatest(info *Info, err error) error {
if info.Namespaced() && len(info.Namespace) == 0 { if info.Namespaced() && len(info.Namespace) == 0 {
return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name) 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 { if err != nil {
return err return err
} }