From 09cfa364c5e67fa87b8114f74001af99782d2554 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 27 Oct 2014 15:56:34 -0400 Subject: [PATCH] Refactor Get and Describe to allow extension of types Get should use ResourceMapper, allow Printer to be abstracted, and extract Describe as *Describer types. --- cmd/kubectl/kubectl.go | 2 +- pkg/client/fake_pods.go | 2 +- pkg/client/fake_services.go | 2 +- pkg/kubectl/cmd/cmd.go | 52 ++- pkg/kubectl/cmd/create.go | 2 +- pkg/kubectl/cmd/delete.go | 2 +- pkg/kubectl/cmd/describe.go | 17 +- pkg/kubectl/cmd/get.go | 33 +- pkg/kubectl/cmd/resource.go | 59 +++- pkg/kubectl/cmd/update.go | 2 +- pkg/kubectl/describe.go | 157 +++++---- pkg/kubectl/describe_test.go | 83 +++++ pkg/kubectl/get.go | 56 ---- pkg/kubectl/kubectl.go | 99 ------ pkg/kubectl/modify_test.go | 63 ---- pkg/kubectl/resource_printer.go | 17 +- pkg/kubectl/{modify.go => resthelper.go} | 39 ++- pkg/kubectl/resthelper_test.go | 405 +++++++++++++++++++++++ 18 files changed, 753 insertions(+), 339 deletions(-) create mode 100644 pkg/kubectl/describe_test.go delete mode 100644 pkg/kubectl/get.go delete mode 100644 pkg/kubectl/modify_test.go rename pkg/kubectl/{modify.go => resthelper.go} (59%) create mode 100644 pkg/kubectl/resthelper_test.go diff --git a/cmd/kubectl/kubectl.go b/cmd/kubectl/kubectl.go index 941ec363df5..ccb2359863f 100644 --- a/cmd/kubectl/kubectl.go +++ b/cmd/kubectl/kubectl.go @@ -23,5 +23,5 @@ import ( ) func main() { - cmd.RunKubectl(os.Stdout) + cmd.NewFactory().Run(os.Stdout) } diff --git a/pkg/client/fake_pods.go b/pkg/client/fake_pods.go index 30ffb8288cb..27e9e7b5551 100644 --- a/pkg/client/fake_pods.go +++ b/pkg/client/fake_pods.go @@ -35,7 +35,7 @@ func (c *FakePods) List(selector labels.Selector) (*api.PodList, error) { func (c *FakePods) Get(name string) (*api.Pod, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-pod", Value: name}) - return &api.Pod{}, nil + return &api.Pod{ObjectMeta: api.ObjectMeta{Name: name, Namespace: c.Namespace}}, nil } func (c *FakePods) Delete(name string) error { diff --git a/pkg/client/fake_services.go b/pkg/client/fake_services.go index 801a1201dd8..5724a94dcce 100644 --- a/pkg/client/fake_services.go +++ b/pkg/client/fake_services.go @@ -36,7 +36,7 @@ func (c *FakeServices) List(selector labels.Selector) (*api.ServiceList, error) func (c *FakeServices) Get(name string) (*api.Service, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-service", Value: name}) - return &api.Service{}, nil + return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: c.Namespace}}, nil } func (c *FakeServices) Create(service *api.Service) (*api.Service, error) { diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 65c88197bfd..c2026de45b1 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -35,13 +35,38 @@ import ( "github.com/spf13/cobra" ) +// Factory provides abstractions that allow the Kubectl command to be extended across multiple types +// of resources and different API sets. type Factory struct { - Mapper meta.RESTMapper - Typer runtime.ObjectTyper - Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) + Describer func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) + Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) } -func RunKubectl(out io.Writer) { +// NewFactory creates a factory with the default Kubernetes resources defined +func NewFactory() *Factory { + return &Factory{ + Mapper: latest.RESTMapper, + Typer: api.Scheme, + Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { + return getKubeClient(cmd), nil + }, + Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { + describer, ok := kubectl.DescriberFor(mapping.Kind, getKubeClient(cmd)) + if !ok { + return nil, fmt.Errorf("No description has been implemented for %q", mapping.Kind) + } + return describer, nil + }, + Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + return kubectl.NewHumanReadablePrinter(noHeaders), nil + }, + } +} + +func (f *Factory) Run(out io.Writer) { // Parent command to which all subcommands are added. cmds := &cobra.Command{ Use: "kubectl", @@ -52,15 +77,6 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, Run: runHelp, } - factory := &Factory{ - Mapper: latest.NewDefaultRESTMapper(), - Typer: api.Scheme, - Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { - // Will handle all resources defined by the command - return getKubeClient(cmd), nil - }, - } - // Globally persistent flags across all subcommands. // TODO Change flag names to consts to allow safer lookup from subcommands. // TODO Add a verbose flag that turns on glog logging. Probably need a way @@ -78,12 +94,12 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(NewCmdVersion(out)) cmds.AddCommand(NewCmdProxy(out)) - cmds.AddCommand(NewCmdGet(out)) - cmds.AddCommand(NewCmdDescribe(out)) - cmds.AddCommand(factory.NewCmdCreate(out)) - cmds.AddCommand(factory.NewCmdUpdate(out)) - cmds.AddCommand(factory.NewCmdDelete(out)) + cmds.AddCommand(f.NewCmdGet(out)) + cmds.AddCommand(f.NewCmdDescribe(out)) + cmds.AddCommand(f.NewCmdCreate(out)) + cmds.AddCommand(f.NewCmdUpdate(out)) + cmds.AddCommand(f.NewCmdDelete(out)) cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(NewCmdLog(out)) diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index ae5094befd7..6f4325185c7 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -47,7 +47,7 @@ Examples: client, err := f.Client(cmd, mapping) checkErr(err) - err = kubectl.NewRESTModifier(client, mapping).Create(namespace, data) + err = kubectl.NewRESTHelper(client, mapping).Create(namespace, data) checkErr(err) fmt.Fprintf(out, "%s\n", name) }, diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index d0923ff9eaa..1a8cf009b21 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -54,7 +54,7 @@ Examples: client, err := f.Client(cmd, mapping) checkErr(err) - err = kubectl.NewRESTModifier(client, mapping).Delete(namespace, name) + err = kubectl.NewRESTHelper(client, mapping).Delete(namespace, name) checkErr(err) fmt.Fprintf(out, "%s\n", name) }, diff --git a/pkg/kubectl/cmd/describe.go b/pkg/kubectl/cmd/describe.go index 863a3a3e26e..b66e8ea6d53 100644 --- a/pkg/kubectl/cmd/describe.go +++ b/pkg/kubectl/cmd/describe.go @@ -17,13 +17,13 @@ limitations under the License. package cmd import ( + "fmt" "io" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) -func NewCmdDescribe(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdDescribe(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "describe ", Short: "Show details of a specific resource", @@ -32,13 +32,14 @@ func NewCmdDescribe(out io.Writer) *cobra.Command { This command joins many API calls together to form a detailed description of a given resource.`, Run: func(cmd *cobra.Command, args []string) { - if len(args) < 2 { - usageError(cmd, "Need to supply a resource and an ID") - } - resource := args[0] - id := args[1] - err := kubectl.Describe(out, getKubeClient(cmd), resource, id) + mapping, namespace, name := ResourceFromArgs(cmd, args, f.Mapper) + + describer, err := f.Describer(cmd, mapping) checkErr(err) + + s, err := describer.Describe(namespace, name) + checkErr(err) + fmt.Fprintf(out, "%s\n", s) }, } return cmd diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 85c769486fd..e4444cd5b64 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -20,12 +20,13 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/spf13/cobra" ) -func NewCmdGet(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdGet(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "get [(-o|--output=)table|json|yaml|template] [-t |--template=] []", + Use: "get [(-o|--output=)console|json|yaml|...] []", Short: "Display one or many resources", Long: `Display one or many resources. @@ -44,20 +45,24 @@ Examples: $ kubectl get -f json pod 1234-56-7890-234234-456456 `, Run: func(cmd *cobra.Command, args []string) { - var resource, id string - if len(args) == 0 { - usageError(cmd, "Need to supply a resource.") - } - if len(args) >= 1 { - resource = args[0] - } - if len(args) >= 2 { - id = args[1] - } + mapping, namespace, name := ResourceOrTypeFromArgs(cmd, args, f.Mapper) + + selector := getFlagString(cmd, "selector") + labels, err := labels.ParseSelector(selector) + checkErr(err) + + client, err := f.Client(cmd, mapping) + checkErr(err) + + obj, err := kubectl.NewRESTHelper(client, mapping).Get(namespace, name, labels) + checkErr(err) + outputFormat := getFlagString(cmd, "output") templateFile := getFlagString(cmd, "template") - selector := getFlagString(cmd, "selector") - err := kubectl.Get(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), resource, id, selector, outputFormat, getFlagBool(cmd, "no-headers"), templateFile) + defaultPrinter, err := f.Printer(cmd, mapping, getFlagBool(cmd, "no-headers")) + checkErr(err) + + err = kubectl.Print(out, obj, outputFormat, templateFile, defaultPrinter) checkErr(err) }, } diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index 8b3435b1d01..570b894482e 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) @@ -37,7 +36,7 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, if len(args) == 2 { resource := args[0] - namespace = api.NamespaceDefault + namespace = getKubeNamespace(cmd) name = args[1] if len(name) == 0 || len(resource) == 0 { usageError(cmd, "Must specify filename or command line params") @@ -63,6 +62,62 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, return } +// ResourceFromArgs expects two arguments with a given type, and extracts the fields necessary +// to uniquely locate a resource. Displays a usageError if that contract is not satisfied, or +// a generic error if any other problems occur. +func ResourceFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string) { + if len(args) != 2 { + usageError(cmd, "Must provide resource and name command line params") + } + + resource := args[0] + namespace = getKubeNamespace(cmd) + name = args[1] + if len(name) == 0 || len(resource) == 0 { + usageError(cmd, "Must provide resource and name command line params") + } + + version, kind, err := mapper.VersionAndKindForResource(resource) + checkErr(err) + + mapping, err = mapper.RESTMapping(version, kind) + checkErr(err) + return +} + +// ResourceFromArgs expects two arguments with a given type, and extracts the fields necessary +// to uniquely locate a resource. Displays a usageError if that contract is not satisfied, or +// a generic error if any other problems occur. +func ResourceOrTypeFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string) { + if len(args) == 0 || len(args) > 2 { + usageError(cmd, "Must provide resource or a resource and name as command line params") + } + + resource := args[0] + if len(resource) == 0 { + usageError(cmd, "Must provide resource or a resource and name as command line params") + } + + namespace = getKubeNamespace(cmd) + if len(args) == 2 { + name = args[1] + if len(name) == 0 { + usageError(cmd, "Must provide resource or a resource and name as command line params") + } + } + + version, kind, err := mapper.VersionAndKindForResource(resource) + checkErr(err) + + mapping, err = mapper.RESTMapping(version, kind) + checkErr(err) + + return +} + +// ResourceFromFile retrieves the name and namespace from a valid file. If the file does not +// resolve to a known type an error is returned. The returned mapping can be used to determine +// the correct REST endpoint to modify this resource with. func ResourceFromFile(filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string, data []byte) { configData, err := readConfigData(filename) checkErr(err) diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index a4f565097e3..a28fc549ad0 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -47,7 +47,7 @@ Examples: client, err := f.Client(cmd, mapping) checkErr(err) - err = kubectl.NewRESTModifier(client, mapping).Update(namespace, name, true, data) + err = kubectl.NewRESTHelper(client, mapping).Update(namespace, name, true, data) checkErr(err) fmt.Fprintf(out, "%s\n", name) }, diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 6697c7b1527..4724534c0e8 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -18,7 +18,6 @@ package kubectl import ( "fmt" - "io" "strings" "text/tabwriter" @@ -28,35 +27,62 @@ import ( "github.com/golang/glog" ) -func Describe(w io.Writer, c client.Interface, resource, id string) error { - var str string - var err error - path, err := resolveResource(resolveToPath, resource) - if err != nil { - return err - } - switch path { - case "pods": - str, err = describePod(w, c, id) - case "replicationControllers": - str, err = describeReplicationController(w, c, id) - case "services": - str, err = describeService(w, c, id) - case "minions": - str, err = describeMinion(w, c, id) - } - - if err != nil { - return err - } - - _, err = fmt.Fprintf(w, str) - return err +// Describer generates output for the named resource or an error +// if the output could not be generated. +type Describer interface { + Describe(namespace, name string) (output string, err error) } -func describePod(w io.Writer, c client.Interface, id string) (string, error) { - // TODO this needs proper namespace support - pod, err := c.Pods(api.NamespaceDefault).Get(id) +// Describer returns the default describe functions for each of the standard +// Kubernetes types. +func DescriberFor(kind string, c *client.Client) (Describer, bool) { + switch kind { + case "Pod": + return &PodDescriber{ + PodClient: func(namespace string) (client.PodInterface, error) { + return c.Pods(namespace), nil + }, + ReplicationControllerClient: func(namespace string) (client.ReplicationControllerInterface, error) { + return c.ReplicationControllers(namespace), nil + }, + }, true + case "ReplicationController": + return &ReplicationControllerDescriber{ + PodClient: func(namespace string) (client.PodInterface, error) { + return c.Pods(namespace), nil + }, + ReplicationControllerClient: func(namespace string) (client.ReplicationControllerInterface, error) { + return c.ReplicationControllers(namespace), nil + }, + }, true + case "Service": + return &ServiceDescriber{ + ServiceClient: func(namespace string) (client.ServiceInterface, error) { + return c.Services(namespace), nil + }, + }, true + } + return nil, false +} + +// PodDescriber generates information about a pod and the replication controllers that +// create it. +type PodDescriber struct { + PodClient func(namespace string) (client.PodInterface, error) + ReplicationControllerClient func(namespace string) (client.ReplicationControllerInterface, error) +} + +func (d *PodDescriber) Describe(namespace, name string) (string, error) { + rc, err := d.ReplicationControllerClient(namespace) + if err != nil { + return "", err + } + pc, err := d.PodClient(namespace) + if err != nil { + return "", err + } + + pod, err := pc.Get(name) if err != nil { return "", err } @@ -67,19 +93,34 @@ func describePod(w io.Writer, c client.Interface, id string) (string, error) { fmt.Fprintf(out, "Host:\t%s\n", pod.CurrentState.Host+"/"+pod.CurrentState.HostIP) fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(pod.Labels)) fmt.Fprintf(out, "Status:\t%s\n", string(pod.CurrentState.Status)) - fmt.Fprintf(out, "Replication Controllers:\t%s\n", getReplicationControllersForLabels(c, labels.Set(pod.Labels))) + fmt.Fprintf(out, "Replication Controllers:\t%s\n", getReplicationControllersForLabels(rc, labels.Set(pod.Labels))) return nil }) } -func describeReplicationController(w io.Writer, c client.Interface, id string) (string, error) { - // TODO this needs proper namespace support - controller, err := c.ReplicationControllers(api.NamespaceDefault).Get(id) +// ReplicationControllerDescriber generates information about a replication controller +// and the pods it has created. +type ReplicationControllerDescriber struct { + ReplicationControllerClient func(namespace string) (client.ReplicationControllerInterface, error) + PodClient func(namespace string) (client.PodInterface, error) +} + +func (d *ReplicationControllerDescriber) Describe(namespace, name string) (string, error) { + rc, err := d.ReplicationControllerClient(namespace) + if err != nil { + return "", err + } + pc, err := d.PodClient(namespace) if err != nil { return "", err } - running, waiting, terminated, err := getPodStatusForReplicationController(c, controller) + controller, err := rc.Get(name) + if err != nil { + return "", err + } + + running, waiting, terminated, err := getPodStatusForReplicationController(pc, controller) if err != nil { return "", err } @@ -95,8 +136,18 @@ func describeReplicationController(w io.Writer, c client.Interface, id string) ( }) } -func describeService(w io.Writer, c client.Interface, id string) (string, error) { - service, err := c.Services(api.NamespaceDefault).Get(id) +// ServiceDescriber generates information about a service. +type ServiceDescriber struct { + ServiceClient func(namespace string) (client.ServiceInterface, error) +} + +func (d *ServiceDescriber) Describe(namespace, name string) (string, error) { + c, err := d.ServiceClient(namespace) + if err != nil { + return "", err + } + + service, err := c.Get(name) if err != nil { return "", err } @@ -110,8 +161,17 @@ func describeService(w io.Writer, c client.Interface, id string) (string, error) }) } -func describeMinion(w io.Writer, c client.Interface, id string) (string, error) { - minion, err := getMinion(c, id) +// MinionDescriber generates information about a minion. +type MinionDescriber struct { + MinionClient func() (client.MinionInterface, error) +} + +func (d *MinionDescriber) Describe(namespace, name string) (string, error) { + mc, err := d.MinionClient() + if err != nil { + return "", err + } + minion, err := mc.Get(name) if err != nil { return "", err } @@ -122,29 +182,14 @@ func describeMinion(w io.Writer, c client.Interface, id string) (string, error) }) } -// client.Interface doesn't have GetMinion(id) yet so we hack it up. -func getMinion(c client.Interface, id string) (*api.Minion, error) { - minionList, err := c.Minions().List() - if err != nil { - glog.Fatalf("Error getting minion info: %v\n", err) - } - - for _, minion := range minionList.Items { - if id == minion.Name { - return &minion, nil - } - } - return nil, fmt.Errorf("Minion %s not found", id) -} - // Get all replication controllers whose selectors would match a given set of // labels. // TODO Move this to pkg/client and ideally implement it server-side (instead // of getting all RC's and searching through them manually). -func getReplicationControllersForLabels(c client.Interface, labelsToMatch labels.Labels) string { +func getReplicationControllersForLabels(c client.ReplicationControllerInterface, labelsToMatch labels.Labels) string { // Get all replication controllers. // TODO this needs a namespace scope as argument - rcs, err := c.ReplicationControllers(api.NamespaceDefault).List(labels.Everything()) + rcs, err := c.List(labels.Everything()) if err != nil { glog.Fatalf("Error getting replication controllers: %v\n", err) } @@ -171,8 +216,8 @@ func getReplicationControllersForLabels(c client.Interface, labelsToMatch labels return list } -func getPodStatusForReplicationController(kubeClient client.Interface, controller *api.ReplicationController) (running, waiting, terminated int, err error) { - rcPods, err := kubeClient.Pods(controller.Namespace).List(labels.SelectorFromSet(controller.DesiredState.ReplicaSelector)) +func getPodStatusForReplicationController(c client.PodInterface, controller *api.ReplicationController) (running, waiting, terminated int, err error) { + rcPods, err := c.List(labels.SelectorFromSet(controller.DesiredState.ReplicaSelector)) if err != nil { return } diff --git a/pkg/kubectl/describe_test.go b/pkg/kubectl/describe_test.go new file mode 100644 index 00000000000..c0b2ac8d057 --- /dev/null +++ b/pkg/kubectl/describe_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +type describeClient struct { + T *testing.T + Namespace string + Err error + Fake *client.Fake +} + +func (c *describeClient) Pod(namespace string) (client.PodInterface, error) { + if namespace != c.Namespace { + c.T.Errorf("unexpected namespace arg: %s", namespace) + } + return c.Fake.Pods(namespace), c.Err +} + +func (c *describeClient) ReplicationController(namespace string) (client.ReplicationControllerInterface, error) { + if namespace != c.Namespace { + c.T.Errorf("unexpected namespace arg: %s", namespace) + } + return c.Fake.ReplicationControllers(namespace), c.Err +} + +func (c *describeClient) Service(namespace string) (client.ServiceInterface, error) { + if namespace != c.Namespace { + c.T.Errorf("unexpected namespace arg: %s", namespace) + } + return c.Fake.Services(namespace), c.Err +} + +func TestDescribePod(t *testing.T) { + fake := &client.Fake{} + c := &describeClient{T: t, Namespace: "foo", Fake: fake} + d := PodDescriber{ + PodClient: c.Pod, + ReplicationControllerClient: c.ReplicationController, + } + out, err := d.Describe("foo", "bar") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !strings.Contains(out, "bar") || !strings.Contains(out, "Status:") { + t.Errorf("unexpected out: %s", out) + } +} + +func TestDescribeService(t *testing.T) { + fake := &client.Fake{} + c := &describeClient{T: t, Namespace: "foo", Fake: fake} + d := ServiceDescriber{ + ServiceClient: c.Service, + } + out, err := d.Describe("foo", "bar") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !strings.Contains(out, "Labels:") || !strings.Contains(out, "bar") { + t.Errorf("unexpected out: %s", out) + } +} diff --git a/pkg/kubectl/get.go b/pkg/kubectl/get.go deleted file mode 100644 index 13d5612dedf..00000000000 --- a/pkg/kubectl/get.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 kubectl - -import ( - "fmt" - "io" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" -) - -func Get(w io.Writer, c *client.RESTClient, namespace string, resource string, id string, selector string, format string, noHeaders bool, templateFile string) error { - path, err := resolveResource(resolveToPath, resource) - if err != nil { - return err - } - - r := c.Verb("GET").Namespace(namespace).Path(path) - if len(id) > 0 { - r.Path(id) - } - if len(selector) > 0 { - r.ParseSelectorParam("labels", selector) - } - result := r.Do() - obj, err := result.Get() - if err != nil { - return err - } - - printer, err := getPrinter(format, templateFile, noHeaders) - if err != nil { - return err - } - - if err = printer.PrintObj(obj, w); err != nil { - body, _ := result.Raw() - return fmt.Errorf("Failed to print: %v\nRaw received object:\n%#v\n\nBody received: %v", err, obj, string(body)) - } - - return nil -} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 7f113fa991b..8ad267feff0 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -31,8 +31,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" - - "gopkg.in/v1/yaml" ) var apiVersionToUse = "v1beta1" @@ -132,16 +130,6 @@ func promptForString(field string, r io.Reader) string { return result } -func CreateResource(resource, id string) ([]byte, error) { - kind, err := resolveResource(resolveToKind, resource) - if err != nil { - return nil, err - } - - s := fmt.Sprintf(`{"kind": "%s", "apiVersion": "%s", "id": "%s"}`, kind, apiVersionToUse, id) - return []byte(s), nil -} - // TODO Move to labels package. func formatLabels(labelMap map[string]string) string { l := labels.Set(labelMap).String() @@ -158,90 +146,3 @@ func makeImageList(manifest api.ContainerManifest) string { } return strings.Join(images, ",") } - -const ( - resolveToPath = "path" - resolveToKind = "kind" -) - -// Takes a human-friendly reference to a resource and converts it to either a -// resource path for an API call or to a Kind to construct a JSON definition. -// See usages of the function for more context. -// -// target is one of the above constants ("path" or "kind") to determine what to -// resolve the resource to. -// -// resource is the human-friendly reference to the resource you want to -// convert. -func resolveResource(target, resource string) (string, error) { - if target != resolveToPath && target != resolveToKind { - return "", fmt.Errorf("Unrecognized target to convert to: %s", target) - } - - var resolved string - var err error - // Caseless comparison. - resource = strings.ToLower(resource) - switch resource { - case "pods", "pod", "po": - if target == resolveToPath { - resolved = "pods" - } else { - resolved = "Pod" - } - case "replicationcontrollers", "replicationcontroller", "rc": - if target == resolveToPath { - resolved = "replicationControllers" - } else { - resolved = "ReplicationController" - } - case "services", "service", "se": - if target == resolveToPath { - resolved = "services" - } else { - resolved = "Service" - } - case "minions", "minion", "mi": - if target == resolveToPath { - resolved = "minions" - } else { - resolved = "Minion" - } - default: - // It might be a GUID, but we don't know how to handle those for now. - err = fmt.Errorf("Resource %s not recognized; need pods, replicationControllers, services or minions.", resource) - } - return resolved, err -} - -func resolveKindToResource(kind string) (resource string, err error) { - // Determine the REST resource according to the type in data. - switch kind { - case "Pod": - resource = "pods" - case "ReplicationController": - resource = "replicationControllers" - case "Service": - resource = "services" - default: - err = fmt.Errorf("Object %s not recognized", kind) - } - return -} - -// versionAndKind will return the APIVersion and Kind of the given wire-format -// enconding of an APIObject, or an error. This is hacked in until the -// migration to v1beta3. -func versionAndKind(data []byte) (version, kind string, err error) { - findKind := struct { - Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` - APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` - }{} - // yaml is a superset of json, so we use it to decode here. That way, - // we understand both. - err = yaml.Unmarshal(data, &findKind) - if err != nil { - return "", "", fmt.Errorf("couldn't get version/kind: %v", err) - } - return findKind.APIVersion, findKind.Kind, nil -} diff --git a/pkg/kubectl/modify_test.go b/pkg/kubectl/modify_test.go deleted file mode 100644 index 8be729d0d44..00000000000 --- a/pkg/kubectl/modify_test.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -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 kubectl - -import ( - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" -) - -type FakeRESTClient struct{} - -func (c *FakeRESTClient) Get() *client.Request { - return &client.Request{} -} -func (c *FakeRESTClient) Put() *client.Request { - return &client.Request{} -} -func (c *FakeRESTClient) Post() *client.Request { - return &client.Request{} -} -func (c *FakeRESTClient) Delete() *client.Request { - return &client.Request{} -} - -func TestRESTModifierDelete(t *testing.T) { - tests := []struct { - Err bool - }{ - /*{ - Err: true, - },*/ - } - for _, test := range tests { - client := &FakeRESTClient{} - modifier := &RESTModifier{ - RESTClient: client, - } - err := modifier.Delete("bar", "foo") - switch { - case err == nil && test.Err: - t.Errorf("Unexpected non-error") - continue - case err != nil && !test.Err: - t.Errorf("Unexpected error: %v", err) - continue - } - } -} diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 20a1e9ad8b8..0fd1cbfb9c5 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -34,7 +34,20 @@ import ( "gopkg.in/v1/yaml" ) -func getPrinter(format, templateFile string, noHeaders bool) (ResourcePrinter, error) { +// Print outputs a runtime.Object to an io.Writer in the given format +func Print(w io.Writer, obj runtime.Object, format string, templateFile string, defaultPrinter ResourcePrinter) error { + printer, err := getPrinter(format, templateFile, defaultPrinter) + if err != nil { + return err + } + + if err := printer.PrintObj(obj, w); err != nil { + return fmt.Errorf("Failed to print: %v\nRaw received object:\n%#v", err, obj) + } + return nil +} + +func getPrinter(format, templateFile string, defaultPrinter ResourcePrinter) (ResourcePrinter, error) { var printer ResourcePrinter switch format { case "json": @@ -60,7 +73,7 @@ func getPrinter(format, templateFile string, noHeaders bool) (ResourcePrinter, e Template: tmpl, } default: - printer = NewHumanReadablePrinter(noHeaders) + printer = defaultPrinter } return printer, nil } diff --git a/pkg/kubectl/modify.go b/pkg/kubectl/resthelper.go similarity index 59% rename from pkg/kubectl/modify.go rename to pkg/kubectl/resthelper.go index 5eda8de1c11..e26f8dbc32c 100644 --- a/pkg/kubectl/modify.go +++ b/pkg/kubectl/resthelper.go @@ -18,12 +18,13 @@ package kubectl import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) -// RESTModifier provides methods for mutating a known or unknown -// RESTful resource. -type RESTModifier struct { +// RESTHelper provides methods for retrieving or mutating a RESTful +// resource. +type RESTHelper struct { Resource string // A RESTClient capable of mutating this resource RESTClient RESTClient @@ -34,9 +35,9 @@ type RESTModifier struct { Versioner runtime.ResourceVersioner } -// NewRESTModifier creates a RESTModifier from a RESTMapping -func NewRESTModifier(client RESTClient, mapping *meta.RESTMapping) *RESTModifier { - return &RESTModifier{ +// NewRESTHelper creates a RESTHelper from a ResourceMapping +func NewRESTHelper(client RESTClient, mapping *meta.RESTMapping) *RESTHelper { + return &RESTHelper{ RESTClient: client, Resource: mapping.Resource, Codec: mapping.Codec, @@ -44,35 +45,39 @@ func NewRESTModifier(client RESTClient, mapping *meta.RESTMapping) *RESTModifier } } -func (m *RESTModifier) Delete(namespace, name string) error { - return m.RESTClient.Delete().Path(m.Resource).Path(name).Do().Error() +func (m *RESTHelper) Get(namespace, name string, selector labels.Selector) (runtime.Object, error) { + return m.RESTClient.Get().Path(m.Resource).Namespace(namespace).Path(name).SelectorParam("labels", selector).Do().Get() } -func (m *RESTModifier) Create(namespace string, data []byte) error { - return m.RESTClient.Post().Path(m.Resource).Body(data).Do().Error() +func (m *RESTHelper) Delete(namespace, name string) error { + return m.RESTClient.Delete().Path(m.Resource).Namespace(namespace).Path(name).Do().Error() } -func (m *RESTModifier) Update(namespace, name string, overwrite bool, data []byte) error { +func (m *RESTHelper) Create(namespace string, data []byte) error { + return m.RESTClient.Post().Path(m.Resource).Namespace(namespace).Body(data).Do().Error() +} + +func (m *RESTHelper) Update(namespace, name string, overwrite bool, data []byte) error { c := m.RESTClient obj, err := m.Codec.Decode(data) if err != nil { // We don't know how to handle this object, but update it anyway - return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() + return updateResource(c, m.Resource, namespace, name, data) } // Attempt to version the object based on client logic. version, err := m.Versioner.ResourceVersion(obj) if err != nil { // We don't know how to version this object, so send it to the server as is - return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() + return updateResource(c, m.Resource, namespace, name, data) } if version == "" && overwrite { // Retrieve the current version of the object to overwrite the server object serverObj, err := c.Get().Path(m.Resource).Path(name).Do().Get() if err != nil { // The object does not exist, but we want it to be created - return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() + return updateResource(c, m.Resource, namespace, name, data) } serverVersion, err := m.Versioner.ResourceVersion(serverObj) if err != nil { @@ -88,5 +93,9 @@ func (m *RESTModifier) Update(namespace, name string, overwrite bool, data []byt data = newData } - return c.Put().Path(m.Resource).Path(name).Body(data).Do().Error() + return updateResource(c, m.Resource, namespace, name, data) +} + +func updateResource(c RESTClient, resourcePath, namespace, name string, data []byte) error { + return c.Put().Path(resourcePath).Namespace(namespace).Path(name).Body(data).Do().Error() } diff --git a/pkg/kubectl/resthelper_test.go b/pkg/kubectl/resthelper_test.go new file mode 100644 index 00000000000..4c8f8687d4a --- /dev/null +++ b/pkg/kubectl/resthelper_test.go @@ -0,0 +1,405 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type httpClientFunc func(*http.Request) (*http.Response, error) + +func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { + return f(req) +} + +type FakeRESTClient struct { + Client client.HTTPClient + Req *http.Request + Resp *http.Response + Err error +} + +func (c *FakeRESTClient) Get() *client.Request { + return client.NewRequest(c, "GET", &url.URL{Host: "localhost"}, testapi.Codec()) +} +func (c *FakeRESTClient) Put() *client.Request { + return client.NewRequest(c, "PUT", &url.URL{Host: "localhost"}, testapi.Codec()) +} +func (c *FakeRESTClient) Post() *client.Request { + return client.NewRequest(c, "POST", &url.URL{Host: "localhost"}, testapi.Codec()) +} +func (c *FakeRESTClient) Delete() *client.Request { + return client.NewRequest(c, "DELETE", &url.URL{Host: "localhost"}, testapi.Codec()) +} +func (c *FakeRESTClient) Do(req *http.Request) (*http.Response, error) { + c.Req = req + if c.Client != client.HTTPClient(nil) { + return c.Client.Do(req) + } + return c.Resp, c.Err +} + +func objBody(obj runtime.Object) io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Codec(), obj)))) +} + +func TestRESTHelperDelete(t *testing.T) { + tests := []struct { + Err bool + Req func(*http.Request) bool + Resp *http.Response + HttpErr error + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Status{Status: api.StatusSuccess}), + }, + Req: func(req *http.Request) bool { + if req.Method != "DELETE" { + t.Errorf("unexpected method: %#v", req) + return false + } + if !strings.HasSuffix(req.URL.Path, "/foo") { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + if req.URL.Query().Get("namespace") != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + }, + }, + } + for _, test := range tests { + client := &FakeRESTClient{ + Resp: test.Resp, + Err: test.HttpErr, + } + modifier := &RESTHelper{ + RESTClient: client, + } + err := modifier.Delete("bar", "foo") + if (err != nil) != test.Err { + t.Errorf("unexpected error: %f %v", test.Err, err) + } + if err != nil { + continue + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("unexpected request: %#v", client.Req) + } + } +} + +func TestRESTHelperCreate(t *testing.T) { + tests := []struct { + Resp *http.Response + HttpErr error + Object runtime.Object + + Err bool + Data []byte + Req func(*http.Request) bool + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Status{Status: api.StatusSuccess}), + }, + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Req: func(req *http.Request) bool { + if req.Method != "POST" { + t.Errorf("unexpected method: %#v", req) + return false + } + if req.URL.Query().Get("namespace") != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + }, + }, + } + for _, test := range tests { + client := &FakeRESTClient{ + Resp: test.Resp, + Err: test.HttpErr, + } + modifier := &RESTHelper{ + RESTClient: client, + } + data := test.Data + if test.Object != nil { + data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object)) + } + err := modifier.Create("bar", data) + if (err != nil) != test.Err { + t.Errorf("unexpected error: %f %v", test.Err, err) + } + if err != nil { + continue + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("unexpected request: %#v", client.Req) + } + if test.Data != nil { + body, _ := ioutil.ReadAll(client.Req.Body) + if !reflect.DeepEqual(test.Data, body) { + t.Errorf("unexpected body: %s", string(body)) + } + } + } +} + +func TestRESTHelperGet(t *testing.T) { + tests := []struct { + Err bool + Req func(*http.Request) bool + Resp *http.Response + HttpErr error + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}), + }, + Req: func(req *http.Request) bool { + if req.Method != "GET" { + t.Errorf("unexpected method: %#v", req) + return false + } + if !strings.HasSuffix(req.URL.Path, "/foo") { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + if req.URL.Query().Get("namespace") != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + }, + }, + } + for _, test := range tests { + client := &FakeRESTClient{ + Resp: test.Resp, + Err: test.HttpErr, + } + modifier := &RESTHelper{ + RESTClient: client, + } + obj, err := modifier.Get("bar", "foo", labels.Everything()) + if (err != nil) != test.Err { + t.Errorf("unexpected error: %f %v", test.Err, err) + } + if err != nil { + continue + } + if obj.(*api.Pod).Name != "foo" { + t.Errorf("unexpected object: %#v", obj) + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("unexpected request: %#v", client.Req) + } + } +} + +func TestRESTHelperUpdate(t *testing.T) { + tests := []struct { + Resp *http.Response + RespFunc httpClientFunc + HttpErr error + Overwrite bool + Object runtime.Object + + ExpectObject runtime.Object + Err bool + Req func(*http.Request) bool + }{ + { + HttpErr: errors.New("failure"), + Err: true, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Resp: &http.Response{ + StatusCode: http.StatusNotFound, + Body: objBody(&api.Status{Status: api.StatusFailure}), + }, + Err: true, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(&api.Status{Status: api.StatusSuccess}), + }, + Req: func(req *http.Request) bool { + if req.Method != "PUT" { + t.Errorf("unexpected method: %#v", req) + return false + } + if !strings.HasSuffix(req.URL.Path, "/foo") { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + if req.URL.Query().Get("namespace") != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + }, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + + Overwrite: true, + RespFunc: func(req *http.Request) (*http.Response, error) { + if req.Method == "PUT" { + return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, nil + } + return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil + }, + Req: func(req *http.Request) bool { + if req.Method != "PUT" { + t.Errorf("unexpected method: %#v", req) + return false + } + if !strings.HasSuffix(req.URL.Path, "/foo") { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + if req.URL.Query().Get("namespace") != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + }, + }, + { + Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}}, + Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, + Req: func(req *http.Request) bool { + if req.Method != "PUT" { + t.Errorf("unexpected method: %#v", req) + return false + } + if !strings.HasSuffix(req.URL.Path, "/foo") { + t.Errorf("url doesn't contain name: %#v", req) + return false + } + if req.URL.Query().Get("namespace") != "bar" { + t.Errorf("url doesn't contain namespace: %#v", req) + return false + } + return true + }, + }, + } + for i, test := range tests { + client := &FakeRESTClient{ + Resp: test.Resp, + Err: test.HttpErr, + } + if test.RespFunc != nil { + client.Client = test.RespFunc + } + modifier := &RESTHelper{ + RESTClient: client, + Codec: testapi.Codec(), + Versioner: testapi.MetadataAccessor(), + } + data := []byte{} + if test.Object != nil { + data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object)) + } + err := modifier.Update("bar", "foo", test.Overwrite, data) + if (err != nil) != test.Err { + t.Errorf("%d: unexpected error: %f %v", i, test.Err, err) + } + if err != nil { + continue + } + if test.Req != nil && !test.Req(client.Req) { + t.Errorf("%d: unexpected request: %#v", i, client.Req) + } + body, err := ioutil.ReadAll(client.Req.Body) + if err != nil { + t.Fatalf("%d: unexpected error: %#v", i, err) + } + t.Logf("got body: %s", string(body)) + expect := []byte{} + if test.ExpectObject != nil { + expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject)) + } + if !reflect.DeepEqual(expect, body) { + t.Errorf("%d: unexpected body: %s", i, string(body)) + } + } +}