diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index f2fd6fb945d..106c6d335ba 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -42,6 +43,7 @@ const ( // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. +// TODO: make the functions interfaces type Factory struct { clients *clientCache flags *pflag.FlagSet @@ -218,6 +220,13 @@ func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { return clientConfig } +// ClientMapperForCommand returns a ClientMapper for the given command and factory. +func ClientMapperForCommand(cmd *cobra.Command, f *Factory) resource.ClientMapper { + return resource.ClientMapperFunc(func(mapping *meta.RESTMapping) (resource.RESTClient, error) { + return f.RESTClient(cmd, mapping) + }) +} + func checkErr(err error) { if err != nil { glog.FatalDepth(1, err) diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 2bc3db5c143..d8dea7ff1fb 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -22,6 +22,8 @@ import ( "io" "io/ioutil" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" . "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" @@ -66,12 +68,12 @@ func newExternalScheme() (*runtime.Scheme, meta.RESTMapper, runtime.Codec) { } type testPrinter struct { - Obj runtime.Object - Err error + Objects []runtime.Object + Err error } func (t *testPrinter) PrintObj(obj runtime.Object, out io.Writer) error { - t.Obj = obj + t.Objects = append(t.Objects, obj) fmt.Fprintf(out, "%#v", obj) return t.Err } @@ -112,6 +114,23 @@ func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { }, t, codec } +func NewAPIFactory() (*Factory, *testFactory, runtime.Codec) { + t := &testFactory{} + return &Factory{ + Mapper: latest.RESTMapper, + Typer: api.Scheme, + RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { + return t.Client, t.Err + }, + Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { + return t.Describer, t.Err + }, + Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + return t.Printer, t.Err + }, + }, t, latest.Codec +} + func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) } diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index a6e408a52e3..ab6bfcd77eb 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -59,7 +59,7 @@ Examples: checkErr(err) selector := GetFlagString(cmd, "selector") found := 0 - ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.RESTClient, schema).Visit(func(r *resource.Info) error { + ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.RESTClient, schema, true).Visit(func(r *resource.Info) error { found++ if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil { return err diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 014a6165769..16b3d1b7f7f 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -20,14 +20,17 @@ import ( "fmt" "io" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/spf13/cobra" ) +// NewCmdGet creates a command object for the generic "get" action, which +// retrieves one or more resources from a server. func (f *Factory) NewCmdGet(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "get [(-o|--output=)json|yaml|...] []", @@ -48,66 +51,134 @@ Examples: $ kubectl get -o json pod 1234-56-7890-234234-456456 - `, + + + $ kubectl get rc,services + `, Run: func(cmd *cobra.Command, args []string) { - mapping, namespace, name := ResourceOrTypeFromArgs(cmd, args, f.Mapper) - - selector := GetFlagString(cmd, "selector") - labelSelector, err := labels.ParseSelector(selector) - checkErr(err) - - client, err := f.RESTClient(cmd, mapping) - checkErr(err) - - outputFormat := GetFlagString(cmd, "output") - templateFile := GetFlagString(cmd, "template") - defaultPrinter, err := f.Printer(cmd, mapping, GetFlagBool(cmd, "no-headers")) - checkErr(err) - - outputVersion := GetFlagString(cmd, "output-version") - if len(outputVersion) == 0 { - outputVersion = mapping.APIVersion - } - - printer, err := kubectl.GetPrinter(outputFormat, templateFile, outputVersion, mapping.ObjectConvertor, defaultPrinter) - checkErr(err) - - restHelper := resource.NewHelper(client, mapping) - var obj runtime.Object - if len(name) == 0 { - obj, err = restHelper.List(namespace, labelSelector) - } else { - obj, err = restHelper.Get(namespace, name) - } - checkErr(err) - - isWatch, isWatchOnly := GetFlagBool(cmd, "watch"), GetFlagBool(cmd, "watch-only") - - // print the current object - if !isWatchOnly { - if err := printer.PrintObj(obj, out); err != nil { - checkErr(fmt.Errorf("unable to output the provided object: %v", err)) - } - } - - // print watched changes - if isWatch || isWatchOnly { - rv, err := mapping.MetadataAccessor.ResourceVersion(obj) - checkErr(err) - - w, err := restHelper.Watch(namespace, rv, labelSelector, labels.Everything()) - checkErr(err) - - kubectl.WatchLoop(w, printer, out) - } + RunGet(f, out, cmd, args) }, } cmd.Flags().StringP("output", "o", "", "Output format: json|yaml|template|templatefile") cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version)") cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers") - cmd.Flags().StringP("template", "t", "", "Template string or path to template file to use when --output=template or --output=templatefile") + cmd.Flags().StringP("template", "t", "", "Template string or path to template file to use when -o=template or -o=templatefile.") cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on") cmd.Flags().BoolP("watch", "w", false, "After listing/getting the requested object, watch for changes.") cmd.Flags().Bool("watch-only", false, "Watch for changes to the requseted object(s), without listing/getting first.") return cmd } + +// RunGet implements the generic Get command +// TODO: convert all direct flag accessors to a struct and pass that instead of cmd +// TODO: return an error instead of using glog.Fatal and checkErr +func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) { + selector := GetFlagString(cmd, "selector") + + // handle watch separately since we cannot watch multiple resource types + isWatch, isWatchOnly := GetFlagBool(cmd, "watch"), GetFlagBool(cmd, "watch-only") + if isWatch || isWatchOnly { + r := resource.NewBuilder(f.Mapper, f.Typer, ClientMapperForCommand(cmd, f)). + NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace(). + SelectorParam(selector). + ResourceTypeOrNameArgs(args...). + SingleResourceType(). + Do() + + mapping, err := r.ResourceMapping() + checkErr(err) + + printer, err := printerForMapping(f, cmd, mapping) + checkErr(err) + + obj, err := r.Object() + checkErr(err) + + rv, err := mapping.MetadataAccessor.ResourceVersion(obj) + checkErr(err) + + // print the current object + if !isWatchOnly { + if err := printer.PrintObj(obj, out); err != nil { + checkErr(fmt.Errorf("unable to output the provided object: %v", err)) + } + } + + // print watched changes + w, err := r.Watch(rv) + checkErr(err) + + kubectl.WatchLoop(w, func(e watch.Event) error { + return printer.PrintObj(e.Object, out) + }) + return + } + + printer, generic, err := printerForCommand(cmd) + checkErr(err) + + b := resource.NewBuilder(f.Mapper, f.Typer, ClientMapperForCommand(cmd, f)). + NamespaceParam(GetKubeNamespace(cmd)).DefaultNamespace(). + SelectorParam(selector). + ResourceTypeOrNameArgs(args...). + Latest() + + if generic { + // the outermost object will be converted to the output-version + printer = kubectl.NewVersionedPrinter(printer, api.Scheme, outputVersion(cmd)) + + obj, err := b.Flatten().Do().Object() + checkErr(err) + + err = printer.PrintObj(obj, out) + checkErr(err) + return + } + + // use the default printer for each object + err = b.Do().Visit(func(r *resource.Info) error { + printer, err := printerForMapping(f, cmd, r.Mapping) + if err != nil { + return err + } + return printer.PrintObj(r.Object, out) + }) + checkErr(err) +} + +// outputVersion returns the preferred output version for generic content (JSON, YAML, or templates) +func outputVersion(cmd *cobra.Command) string { + outputVersion := GetFlagString(cmd, "output-version") + if len(outputVersion) == 0 { + outputVersion = GetFlagString(cmd, "api-version") + } + return outputVersion +} + +// printerForCommand returns the default printer for this command. +func printerForCommand(cmd *cobra.Command) (kubectl.ResourcePrinter, bool, error) { + outputFormat := GetFlagString(cmd, "output") + templateFile := GetFlagString(cmd, "template") + if len(outputFormat) == 0 && len(templateFile) != 0 { + outputFormat = "template" + } + + return kubectl.GetPrinter(outputFormat, templateFile) +} + +// printerForMapping returns a printer suitable for displaying the provided resource type. +func printerForMapping(f *Factory, cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.ResourcePrinter, error) { + printer, ok, err := printerForCommand(cmd) + if err != nil { + return nil, err + } + if ok { + printer = kubectl.NewVersionedPrinter(printer, mapping.ObjectConvertor, outputVersion(cmd)) + } else { + printer, err = f.Printer(cmd, mapping, GetFlagBool(cmd, "no-headers")) + if err != nil { + return nil, err + } + } + return printer, nil +} diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index f108e711414..d0ea2f0920c 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -19,6 +19,8 @@ package cmd_test import ( "bytes" "fmt" + "io" + "io/ioutil" "net/http" "reflect" "strings" @@ -27,9 +29,38 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" ) +func testData() (*api.PodList, *api.ServiceList) { + pods := &api.PodList{ + ListMeta: api.ListMeta{ + ResourceVersion: "15", + }, + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"}, + }, + { + ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "test", ResourceVersion: "11"}, + }, + }, + } + svc := &api.ServiceList{ + ListMeta: api.ListMeta{ + ResourceVersion: "16", + }, + Items: []api.Service{ + { + ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + }, + }, + } + return pods, svc +} + // Verifies that schemas that are not in the master tree of Kubernetes can be retrieved via Get. func TestGetUnknownSchemaObject(t *testing.T) { f, tf, codec := NewTestFactory() @@ -41,12 +72,13 @@ func TestGetUnknownSchemaObject(t *testing.T) { buf := bytes.NewBuffer([]byte{}) cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) cmd.Flags().String("api-version", "default", "") cmd.Flags().String("namespace", "test", "") cmd.Run(cmd, []string{"type", "foo"}) expected := &internalType{Name: "foo"} - actual := tf.Printer.(*testPrinter).Obj + actual := tf.Printer.(*testPrinter).Objects[0] if !reflect.DeepEqual(expected, actual) { t.Errorf("unexpected object: %#v", actual) } @@ -73,12 +105,347 @@ func TestGetSchemaObject(t *testing.T) { cmd.Flags().String("namespace", "test", "") cmd.Run(cmd, []string{"replicationcontrollers", "foo"}) - expected := &api.ReplicationController{ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ReplicationControllerSpec{Template: &api.PodTemplateSpec{}}} - actual := tf.Printer.(*testPrinter).Obj - if !reflect.DeepEqual(expected, actual) { - t.Errorf("unexpected object: %s", util.ObjectGoPrintDiff(expected, actual)) - } if !strings.Contains(buf.String(), "\"foo\"") { t.Errorf("unexpected output: %s", buf.String()) } } + +func TestGetObjects(t *testing.T) { + pods, _ := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Resp: &http.Response{StatusCode: 200, Body: objBody(codec, &pods.Items[0])}, + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := []runtime.Object{&pods.Items[0]} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v", actual) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func TestGetListObjects(t *testing.T) { + pods, _ := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Resp: &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Run(cmd, []string{"pods"}) + + expected := []runtime.Object{pods} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v %#v", expected, actual) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func TestGetMultipleTypeObjects(t *testing.T) { + pods, svc := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/ns/test/pods": + return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil + case "/ns/test/services": + return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Run(cmd, []string{"pods,services"}) + + expected := []runtime.Object{pods, svc} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v", actual) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func TestGetMultipleTypeObjectsAsList(t *testing.T) { + pods, svc := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/ns/test/pods": + return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil + case "/ns/test/services": + return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + cmd.Flags().String("api-version", "v1beta1", "") + + cmd.Flags().Set("output", "json") + cmd.Run(cmd, []string{"pods,services"}) + + if tf.Printer.(*testPrinter).Objects != nil { + t.Errorf("unexpected print to default printer") + } + + out, err := codec.Decode(buf.Bytes()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := &api.List{ + Items: []runtime.Object{ + &pods.Items[0], + &pods.Items[1], + &svc.Items[0], + }, + } + if !reflect.DeepEqual(expected, out) { + t.Errorf("unexpected output: %#v", out) + } +} + +func TestGetMultipleTypeObjectsWithSelector(t *testing.T) { + pods, svc := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Query().Get("labels") != "a=b" { + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + } + switch req.URL.Path { + case "/ns/test/pods": + return &http.Response{StatusCode: 200, Body: objBody(codec, pods)}, nil + case "/ns/test/services": + return &http.Response{StatusCode: 200, Body: objBody(codec, svc)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + + cmd.Flags().Set("selector", "a=b") + cmd.Run(cmd, []string{"pods,services"}) + + expected := []runtime.Object{pods, svc} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v", actual) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func watchTestData() ([]api.Pod, []watch.Event) { + pods := []api.Pod{ + { + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "test", + ResourceVersion: "10", + }, + }, + } + events := []watch.Event{ + { + Type: watch.Modified, + Object: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "test", + ResourceVersion: "11", + }, + }, + }, + { + Type: watch.Deleted, + Object: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "test", + ResourceVersion: "12", + }, + }, + }, + } + return pods, events +} + +func TestWatchSelector(t *testing.T) { + pods, events := watchTestData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.Query().Get("labels") != "a=b" { + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + } + switch req.URL.Path { + case "/ns/test/pods": + return &http.Response{StatusCode: 200, Body: objBody(codec, &api.PodList{Items: pods})}, nil + case "/watch/ns/test/pods": + return &http.Response{StatusCode: 200, Body: watchBody(codec, events)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + + cmd.Flags().Set("watch", "true") + cmd.Flags().Set("selector", "a=b") + cmd.Run(cmd, []string{"pods"}) + + expected := []runtime.Object{&api.PodList{Items: pods}, events[0].Object, events[1].Object} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v %#v", expected[0], actual[0]) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func TestWatchResource(t *testing.T) { + pods, events := watchTestData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/ns/test/pods/foo": + return &http.Response{StatusCode: 200, Body: objBody(codec, &pods[0])}, nil + case "/watch/ns/test/pods/foo": + return &http.Response{StatusCode: 200, Body: watchBody(codec, events)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + + cmd.Flags().Set("watch", "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := []runtime.Object{&pods[0], events[0].Object, events[1].Object} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v", actual) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func TestWatchOnlyResource(t *testing.T) { + pods, events := watchTestData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/ns/test/pods/foo": + return &http.Response{StatusCode: 200, Body: objBody(codec, &pods[0])}, nil + case "/watch/ns/test/pods/foo": + return &http.Response{StatusCode: 200, Body: watchBody(codec, events)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + buf := bytes.NewBuffer([]byte{}) + + cmd := f.NewCmdGet(buf) + cmd.SetOutput(buf) + cmd.Flags().String("namespace", "test", "") + + cmd.Flags().Set("watch-only", "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := []runtime.Object{events[0].Object, events[1].Object} + actual := tf.Printer.(*testPrinter).Objects + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %#v", actual) + } + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func watchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser { + buf := bytes.NewBuffer([]byte{}) + enc := json.NewEncoder(buf, codec) + for i := range events { + enc.Encode(&events[i]) + } + return ioutil.NopCloser(buf) +} diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index afee120932b..43a41fee79a 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -41,15 +41,23 @@ func ResourcesFromArgsOrFile( mapper meta.RESTMapper, clientBuilder func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error), schema validation.Schema, + requireNames bool, ) resource.Visitor { // handling filename & resource id if len(selector) == 0 { - mapping, namespace, name := ResourceFromArgsOrFile(cmd, args, filename, typer, mapper, schema) - client, err := clientBuilder(cmd, mapping) - checkErr(err) - - return resource.NewInfo(client, mapping, namespace, name) + if requireNames || len(filename) > 0 { + mapping, namespace, name := ResourceFromArgsOrFile(cmd, args, filename, typer, mapper, schema) + client, err := clientBuilder(cmd, mapping) + checkErr(err) + return resource.NewInfo(client, mapping, namespace, name) + } + if len(args) == 2 { + mapping, namespace, name := ResourceOrTypeFromArgs(cmd, args, mapper) + client, err := clientBuilder(cmd, mapping) + checkErr(err) + return resource.NewInfo(client, mapping, namespace, name) + } } labelSelector, err := labels.ParseSelector(selector) @@ -58,9 +66,12 @@ func ResourcesFromArgsOrFile( namespace := GetKubeNamespace(cmd) visitors := resource.VisitorList{} - if len(args) != 1 { + if len(args) < 1 { usageError(cmd, "Must specify the type of resource") } + if len(args) > 1 { + usageError(cmd, "Too many arguments") + } types := SplitResourceArgument(args[0]) for _, arg := range types { resourceName := arg diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 60d28933b5e..f8619aadb89 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -35,66 +35,94 @@ import ( "github.com/golang/glog" ) -// GetPrinter takes a format type, an optional format argument, a version and a convertor -// to be used if the underlying printer requires the object to be in a specific schema ( -// any of the generic formatters), and the default printer to use for this object. -func GetPrinter(format, formatArgument, version string, convertor runtime.ObjectConvertor, defaultPrinter ResourcePrinter) (ResourcePrinter, error) { +// GetPrinter takes a format type, an optional format argument. It will return true +// if the format is generic (untyped), otherwise it will return false. The printer +// is agnostic to schema versions, so you must send arguments to PrintObj in the +// version you wish them to be shown using a VersionedPrinter (typically when +// generic is true). +func GetPrinter(format, formatArgument string) (ResourcePrinter, bool, error) { var printer ResourcePrinter switch format { case "json": - printer = &JSONPrinter{version, convertor} + printer = &JSONPrinter{} case "yaml": - printer = &YAMLPrinter{version, convertor} + printer = &YAMLPrinter{} case "template": if len(formatArgument) == 0 { - return nil, fmt.Errorf("template format specified but no template given") + return nil, false, fmt.Errorf("template format specified but no template given") } var err error - printer, err = NewTemplatePrinter([]byte(formatArgument), version, convertor) + printer, err = NewTemplatePrinter([]byte(formatArgument)) if err != nil { - return nil, fmt.Errorf("error parsing template %s, %v\n", formatArgument, err) + return nil, false, fmt.Errorf("error parsing template %s, %v\n", formatArgument, err) } case "templatefile": if len(formatArgument) == 0 { - return nil, fmt.Errorf("templatefile format specified but no template file given") + return nil, false, fmt.Errorf("templatefile format specified but no template file given") } data, err := ioutil.ReadFile(formatArgument) if err != nil { - return nil, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) + return nil, false, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) } - printer, err = NewTemplatePrinter(data, version, convertor) + printer, err = NewTemplatePrinter(data) if err != nil { - return nil, fmt.Errorf("error parsing template %s, %v\n", string(data), err) + return nil, false, fmt.Errorf("error parsing template %s, %v\n", string(data), err) } case "": - printer = defaultPrinter + return nil, false, nil default: - return nil, fmt.Errorf("output format %q not recognized", format) + return nil, false, fmt.Errorf("output format %q not recognized", format) } - return printer, nil + return printer, true, nil } // ResourcePrinter is an interface that knows how to print runtime objects. type ResourcePrinter interface { - // Print receives an arbitrary object, formats it and prints it to a writer. + // Print receives a runtime object, formats it and prints it to a writer. PrintObj(runtime.Object, io.Writer) error } -// JSONPrinter is an implementation of ResourcePrinter which outputs an object as JSON. -// The input object is assumed to be in the internal version of an API and is converted -// to the given version first. -type JSONPrinter struct { - version string +// ResourcePrinterFunc is a function that can print objects +type ResourcePrinterFunc func(runtime.Object, io.Writer) error + +// PrintObj implements ResourcePrinter +func (fn ResourcePrinterFunc) PrintObj(obj runtime.Object, w io.Writer) error { + return fn(obj, w) +} + +// VersionedPrinter takes runtime objects and ensures they are converted to a given API version +// prior to being passed to a nested printer. +type VersionedPrinter struct { + printer ResourcePrinter convertor runtime.ObjectConvertor + version string +} + +// NewVersionedPrinter wraps a printer to convert objects to a known API version prior to printing. +func NewVersionedPrinter(printer ResourcePrinter, convertor runtime.ObjectConvertor, version string) ResourcePrinter { + return &VersionedPrinter{ + printer: printer, + convertor: convertor, + version: version, + } +} + +// PrintObj implements ResourcePrinter +func (p *VersionedPrinter) PrintObj(obj runtime.Object, w io.Writer) error { + converted, err := p.convertor.ConvertToVersion(obj, p.version) + if err != nil { + return err + } + return p.printer.PrintObj(converted, w) +} + +// JSONPrinter is an implementation of ResourcePrinter which outputs an object as JSON. +type JSONPrinter struct { } // PrintObj is an implementation of ResourcePrinter.PrintObj which simply writes the object to the Writer. func (p *JSONPrinter) PrintObj(obj runtime.Object, w io.Writer) error { - outObj, err := p.convertor.ConvertToVersion(obj, p.version) - if err != nil { - return err - } - data, err := json.Marshal(outObj) + data, err := json.Marshal(obj) if err != nil { return err } @@ -115,11 +143,7 @@ type YAMLPrinter struct { // PrintObj prints the data as YAML. func (p *YAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error { - outObj, err := p.convertor.ConvertToVersion(obj, p.version) - if err != nil { - return err - } - output, err := yaml.Marshal(outObj) + output, err := yaml.Marshal(obj) if err != nil { return err } @@ -387,11 +411,9 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er type TemplatePrinter struct { rawTemplate string template *template.Template - version string - convertor runtime.ObjectConvertor } -func NewTemplatePrinter(tmpl []byte, asVersion string, convertor runtime.ObjectConvertor) (*TemplatePrinter, error) { +func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) { t, err := template.New("output"). Funcs(template.FuncMap{"exists": exists}). Parse(string(tmpl)) @@ -401,18 +423,12 @@ func NewTemplatePrinter(tmpl []byte, asVersion string, convertor runtime.ObjectC return &TemplatePrinter{ rawTemplate: string(tmpl), template: t, - version: asVersion, - convertor: convertor, }, nil } // PrintObj formats the obj with the Go Template. func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { - outObj, err := p.convertor.ConvertToVersion(obj, p.version) - if err != nil { - return err - } - data, err := json.Marshal(outObj) + data, err := json.Marshal(obj) if err != nil { return err } diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go index c8972a054a3..8603cedb0c2 100644 --- a/pkg/kubectl/resource_printer_test.go +++ b/pkg/kubectl/resource_printer_test.go @@ -27,6 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -56,38 +57,56 @@ var testData = testStruct{ IntList: []int{1, 2, 3}, } +func TestVersionedPrinter(t *testing.T) { + original := &testStruct{Key: "value"} + p := NewVersionedPrinter( + ResourcePrinterFunc(func(obj runtime.Object, w io.Writer) error { + if obj == original { + t.Fatalf("object should not be identical: %#v", obj) + } + if obj.(*testStruct).Key != "value" { + t.Fatalf("object was not converted: %#v", obj) + } + return nil + }), + api.Scheme, + testapi.Version(), + ) + if err := p.PrintObj(original, nil); err != nil { + t.Errorf("unexpected error: %v", err) + } +} + func TestYAMLPrinter(t *testing.T) { - testPrinter(t, &YAMLPrinter{testapi.Version(), api.Scheme}, yaml.Unmarshal) + testPrinter(t, &YAMLPrinter{}, yaml.Unmarshal) } func TestJSONPrinter(t *testing.T) { - testPrinter(t, &JSONPrinter{testapi.Version(), api.Scheme}, json.Unmarshal) + testPrinter(t, &JSONPrinter{}, json.Unmarshal) +} + +func TestPrintDefault(t *testing.T) { + printer, found, err := GetPrinter("", "") + if err != nil { + t.Fatalf("unexpected error: %#v", err) + } + if found { + t.Errorf("no printer should have been found: %#v / %v", printer, err) + } } type internalType struct { Name string - Kind string } -type externalType struct { - Name string - Kind string `json:"kind"` +func (*internalType) IsAnAPIObject() { + } -func (*internalType) IsAnAPIObject() {} -func (*externalType) IsAnAPIObject() {} - -func newExternalScheme() *runtime.Scheme { - scheme := runtime.NewScheme() - scheme.AddKnownTypeWithName("", "Type", &internalType{}) - scheme.AddKnownTypeWithName("unlikelyversion", "Type", &externalType{}) - return scheme -} - -func TestPrintJSONForUnknownSchema(t *testing.T) { +func TestPrintJSONForObject(t *testing.T) { buf := bytes.NewBuffer([]byte{}) - printer, err := GetPrinter("json", "", "unlikelyversion", newExternalScheme(), nil) - if err != nil { + printer, found, err := GetPrinter("json", "") + if err != nil || !found { t.Fatalf("unexpected error: %#v", err) } if err := printer.PrintObj(&internalType{Name: "foo"}, buf); err != nil { @@ -102,21 +121,10 @@ func TestPrintJSONForUnknownSchema(t *testing.T) { } } -func TestPrintJSONForUnknownSchemaAndWrongVersion(t *testing.T) { - buf := bytes.NewBuffer([]byte{}) - printer, err := GetPrinter("json", "", "badversion", newExternalScheme(), nil) - if err != nil { - t.Fatalf("unexpected error: %#v", err) - } - if err := printer.PrintObj(&internalType{Name: "foo"}, buf); err == nil { - t.Errorf("unexpected non-error") - } -} - func TestPrintJSON(t *testing.T) { buf := bytes.NewBuffer([]byte{}) - printer, err := GetPrinter("json", "", testapi.Version(), api.Scheme, nil) - if err != nil { + printer, found, err := GetPrinter("json", "") + if err != nil || !found { t.Fatalf("unexpected error: %#v", err) } printer.PrintObj(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, buf) @@ -128,8 +136,8 @@ func TestPrintJSON(t *testing.T) { func TestPrintYAML(t *testing.T) { buf := bytes.NewBuffer([]byte{}) - printer, err := GetPrinter("yaml", "", testapi.Version(), api.Scheme, nil) - if err != nil { + printer, found, err := GetPrinter("yaml", "") + if err != nil || !found { t.Fatalf("unexpected error: %#v", err) } printer.PrintObj(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, buf) @@ -141,11 +149,11 @@ func TestPrintYAML(t *testing.T) { func TestPrintTemplate(t *testing.T) { buf := bytes.NewBuffer([]byte{}) - printer, err := GetPrinter("template", "{{.id}}", "v1beta1", api.Scheme, nil) - if err != nil { + printer, found, err := GetPrinter("template", "{{.id}}") + if err != nil || !found { t.Fatalf("unexpected error: %#v", err) } - err = printer.PrintObj(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, buf) + err = printer.PrintObj(&v1beta1.Pod{TypeMeta: v1beta1.TypeMeta{ID: "foo"}}, buf) if err != nil { t.Fatalf("unexpected error: %#v", err) } @@ -155,19 +163,19 @@ func TestPrintTemplate(t *testing.T) { } func TestPrintEmptyTemplate(t *testing.T) { - if _, err := GetPrinter("template", "", testapi.Version(), api.Scheme, nil); err == nil { + if _, _, err := GetPrinter("template", ""); err == nil { t.Errorf("unexpected non-error") } } func TestPrintBadTemplate(t *testing.T) { - if _, err := GetPrinter("template", "{{ .Name", testapi.Version(), api.Scheme, nil); err == nil { + if _, _, err := GetPrinter("template", "{{ .Name"); err == nil { t.Errorf("unexpected non-error") } } func TestPrintBadTemplateFile(t *testing.T) { - if _, err := GetPrinter("templatefile", "", testapi.Version(), api.Scheme, nil); err == nil { + if _, _, err := GetPrinter("templatefile", ""); err == nil { t.Errorf("unexpected non-error") } } @@ -276,12 +284,17 @@ func TestUnknownTypePrinting(t *testing.T) { func TestTemplateEmitsVersionedObjects(t *testing.T) { // kind is always blank in memory and set on the wire - printer, err := NewTemplatePrinter([]byte(`{{.kind}}`), testapi.Version(), api.Scheme) + printer, err := NewTemplatePrinter([]byte(`{{.kind}}`)) if err != nil { t.Fatalf("tmpl fail: %v", err) } + obj, err := api.Scheme.ConvertToVersion(&api.Pod{}, "v1beta1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + buffer := &bytes.Buffer{} - err = printer.PrintObj(&api.Pod{}, buffer) + err = printer.PrintObj(obj, buffer) if err != nil { t.Fatalf("print fail: %v", err) } @@ -292,7 +305,7 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) { func TestTemplatePanic(t *testing.T) { tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}` - printer, err := NewTemplatePrinter([]byte(tmpl), testapi.Version(), api.Scheme) + printer, err := NewTemplatePrinter([]byte(tmpl)) if err != nil { t.Fatalf("tmpl fail: %v", err) } @@ -396,11 +409,13 @@ e: {{exists . "currentState" "info" "update-demo" "state" "running"}} f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}` _ = useThisToDebug // don't complain about unused var - printer, err := NewTemplatePrinter([]byte(tmpl), "v1beta1", api.Scheme) + p, err := NewTemplatePrinter([]byte(tmpl)) if err != nil { t.Fatalf("tmpl fail: %v", err) } + printer := NewVersionedPrinter(p, api.Scheme, "v1beta1") + for name, item := range table { buffer := &bytes.Buffer{} err = printer.PrintObj(&item.pod, buffer) @@ -416,19 +431,19 @@ f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"} func TestPrinters(t *testing.T) { om := func(name string) api.ObjectMeta { return api.ObjectMeta{Name: name} } - templatePrinter, err := NewTemplatePrinter([]byte("{{.name}}"), testapi.Version(), api.Scheme) + templatePrinter, err := NewTemplatePrinter([]byte("{{.name}}")) if err != nil { t.Fatal(err) } - templatePrinter2, err := NewTemplatePrinter([]byte("{{len .items}}"), testapi.Version(), api.Scheme) + templatePrinter2, err := NewTemplatePrinter([]byte("{{len .items}}")) if err != nil { t.Fatal(err) } printers := map[string]ResourcePrinter{ "humanReadable": NewHumanReadablePrinter(true), "humanReadableHeaders": NewHumanReadablePrinter(false), - "json": &JSONPrinter{testapi.Version(), api.Scheme}, - "yaml": &YAMLPrinter{testapi.Version(), api.Scheme}, + "json": &JSONPrinter{}, + "yaml": &YAMLPrinter{}, "template": templatePrinter, "template2": templatePrinter2, } diff --git a/pkg/kubectl/watchloop.go b/pkg/kubectl/watchloop.go index 2e6d3b9f88b..52eaa4fd0d8 100644 --- a/pkg/kubectl/watchloop.go +++ b/pkg/kubectl/watchloop.go @@ -17,16 +17,15 @@ limitations under the License. package kubectl import ( - "io" "os" "os/signal" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) -// WatchLoop loops, writing objects in the events from w to printer. +// WatchLoop loops, passing events in w to fn. // If user sends interrupt signal, shut down cleanly. Otherwise, never return. -func WatchLoop(w watch.Interface, printer ResourcePrinter, out io.Writer) { +func WatchLoop(w watch.Interface, fn func(watch.Event) error) { signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) defer signal.Stop(signals) @@ -36,8 +35,7 @@ func WatchLoop(w watch.Interface, printer ResourcePrinter, out io.Writer) { if !ok { return } - // TODO: need to print out added/modified/deleted! - if err := printer.PrintObj(event.Object, out); err != nil { + if err := fn(event); err != nil { w.Stop() } case <-signals: