Refactor Get and Describe to allow extension of types

Get should use ResourceMapper, allow Printer to be abstracted,
and extract Describe as *Describer types.
pull/6/head
Clayton Coleman 2014-10-27 15:56:34 -04:00
parent 39882a3555
commit 09cfa364c5
18 changed files with 753 additions and 339 deletions

View File

@ -23,5 +23,5 @@ import (
) )
func main() { func main() {
cmd.RunKubectl(os.Stdout) cmd.NewFactory().Run(os.Stdout)
} }

View File

@ -35,7 +35,7 @@ func (c *FakePods) List(selector labels.Selector) (*api.PodList, error) {
func (c *FakePods) Get(name string) (*api.Pod, error) { func (c *FakePods) Get(name string) (*api.Pod, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-pod", Value: name}) 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 { func (c *FakePods) Delete(name string) error {

View File

@ -36,7 +36,7 @@ func (c *FakeServices) List(selector labels.Selector) (*api.ServiceList, error)
func (c *FakeServices) Get(name string) (*api.Service, error) { func (c *FakeServices) Get(name string) (*api.Service, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-service", Value: name}) 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) { func (c *FakeServices) Create(service *api.Service) (*api.Service, error) {

View File

@ -35,13 +35,38 @@ import (
"github.com/spf13/cobra" "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 { type Factory struct {
Mapper meta.RESTMapper Mapper meta.RESTMapper
Typer runtime.ObjectTyper Typer runtime.ObjectTyper
Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) 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. // Parent command to which all subcommands are added.
cmds := &cobra.Command{ cmds := &cobra.Command{
Use: "kubectl", Use: "kubectl",
@ -52,15 +77,6 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
Run: runHelp, 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. // Globally persistent flags across all subcommands.
// TODO Change flag names to consts to allow safer lookup from 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 // 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(NewCmdVersion(out))
cmds.AddCommand(NewCmdProxy(out)) cmds.AddCommand(NewCmdProxy(out))
cmds.AddCommand(NewCmdGet(out))
cmds.AddCommand(NewCmdDescribe(out))
cmds.AddCommand(factory.NewCmdCreate(out)) cmds.AddCommand(f.NewCmdGet(out))
cmds.AddCommand(factory.NewCmdUpdate(out)) cmds.AddCommand(f.NewCmdDescribe(out))
cmds.AddCommand(factory.NewCmdDelete(out)) cmds.AddCommand(f.NewCmdCreate(out))
cmds.AddCommand(f.NewCmdUpdate(out))
cmds.AddCommand(f.NewCmdDelete(out))
cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(NewCmdNamespace(out))
cmds.AddCommand(NewCmdLog(out)) cmds.AddCommand(NewCmdLog(out))

View File

@ -47,7 +47,7 @@ Examples:
client, err := f.Client(cmd, mapping) client, err := f.Client(cmd, mapping)
checkErr(err) checkErr(err)
err = kubectl.NewRESTModifier(client, mapping).Create(namespace, data) err = kubectl.NewRESTHelper(client, mapping).Create(namespace, data)
checkErr(err) checkErr(err)
fmt.Fprintf(out, "%s\n", name) fmt.Fprintf(out, "%s\n", name)
}, },

View File

@ -54,7 +54,7 @@ Examples:
client, err := f.Client(cmd, mapping) client, err := f.Client(cmd, mapping)
checkErr(err) checkErr(err)
err = kubectl.NewRESTModifier(client, mapping).Delete(namespace, name) err = kubectl.NewRESTHelper(client, mapping).Delete(namespace, name)
checkErr(err) checkErr(err)
fmt.Fprintf(out, "%s\n", name) fmt.Fprintf(out, "%s\n", name)
}, },

View File

@ -17,13 +17,13 @@ limitations under the License.
package cmd package cmd
import ( import (
"fmt"
"io" "io"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func NewCmdDescribe(out io.Writer) *cobra.Command { func (f *Factory) NewCmdDescribe(out io.Writer) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "describe <resource> <id>", Use: "describe <resource> <id>",
Short: "Show details of a specific resource", 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 This command joins many API calls together to form a detailed description of a
given resource.`, given resource.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 { mapping, namespace, name := ResourceFromArgs(cmd, args, f.Mapper)
usageError(cmd, "Need to supply a resource and an ID")
} describer, err := f.Describer(cmd, mapping)
resource := args[0]
id := args[1]
err := kubectl.Describe(out, getKubeClient(cmd), resource, id)
checkErr(err) checkErr(err)
s, err := describer.Describe(namespace, name)
checkErr(err)
fmt.Fprintf(out, "%s\n", s)
}, },
} }
return cmd return cmd

View File

@ -20,12 +20,13 @@ import (
"io" "io"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func NewCmdGet(out io.Writer) *cobra.Command { func (f *Factory) NewCmdGet(out io.Writer) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "get [(-o|--output=)table|json|yaml|template] [-t <file>|--template=<file>] <resource> [<id>]", Use: "get [(-o|--output=)console|json|yaml|...] <resource> [<id>]",
Short: "Display one or many resources", Short: "Display one or many resources",
Long: `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 $ kubectl get -f json pod 1234-56-7890-234234-456456
<list single pod in json output format>`, <list single pod in json output format>`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var resource, id string mapping, namespace, name := ResourceOrTypeFromArgs(cmd, args, f.Mapper)
if len(args) == 0 {
usageError(cmd, "Need to supply a resource.") selector := getFlagString(cmd, "selector")
} labels, err := labels.ParseSelector(selector)
if len(args) >= 1 { checkErr(err)
resource = args[0]
} client, err := f.Client(cmd, mapping)
if len(args) >= 2 { checkErr(err)
id = args[1]
} obj, err := kubectl.NewRESTHelper(client, mapping).Get(namespace, name, labels)
checkErr(err)
outputFormat := getFlagString(cmd, "output") outputFormat := getFlagString(cmd, "output")
templateFile := getFlagString(cmd, "template") templateFile := getFlagString(cmd, "template")
selector := getFlagString(cmd, "selector") defaultPrinter, err := f.Printer(cmd, mapping, getFlagBool(cmd, "no-headers"))
err := kubectl.Get(out, getKubeClient(cmd).RESTClient, getKubeNamespace(cmd), resource, id, selector, outputFormat, getFlagBool(cmd, "no-headers"), templateFile) checkErr(err)
err = kubectl.Print(out, obj, outputFormat, templateFile, defaultPrinter)
checkErr(err) checkErr(err)
}, },
} }

View File

@ -21,7 +21,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
) )
@ -37,7 +36,7 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string,
if len(args) == 2 { if len(args) == 2 {
resource := args[0] resource := args[0]
namespace = api.NamespaceDefault namespace = getKubeNamespace(cmd)
name = args[1] name = args[1]
if len(name) == 0 || len(resource) == 0 { if len(name) == 0 || len(resource) == 0 {
usageError(cmd, "Must specify filename or command line params") usageError(cmd, "Must specify filename or command line params")
@ -63,6 +62,62 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string,
return 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) { func ResourceFromFile(filename string, typer runtime.ObjectTyper, mapper meta.RESTMapper) (mapping *meta.RESTMapping, namespace, name string, data []byte) {
configData, err := readConfigData(filename) configData, err := readConfigData(filename)
checkErr(err) checkErr(err)

View File

@ -47,7 +47,7 @@ Examples:
client, err := f.Client(cmd, mapping) client, err := f.Client(cmd, mapping)
checkErr(err) checkErr(err)
err = kubectl.NewRESTModifier(client, mapping).Update(namespace, name, true, data) err = kubectl.NewRESTHelper(client, mapping).Update(namespace, name, true, data)
checkErr(err) checkErr(err)
fmt.Fprintf(out, "%s\n", name) fmt.Fprintf(out, "%s\n", name)
}, },

View File

@ -18,7 +18,6 @@ package kubectl
import ( import (
"fmt" "fmt"
"io"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
@ -28,35 +27,62 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
) )
func Describe(w io.Writer, c client.Interface, resource, id string) error { // Describer generates output for the named resource or an error
var str string // if the output could not be generated.
var err error type Describer interface {
path, err := resolveResource(resolveToPath, resource) Describe(namespace, name string) (output string, err error)
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
} }
func describePod(w io.Writer, c client.Interface, id string) (string, error) { // Describer returns the default describe functions for each of the standard
// TODO this needs proper namespace support // Kubernetes types.
pod, err := c.Pods(api.NamespaceDefault).Get(id) 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 { if err != nil {
return "", err 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, "Host:\t%s\n", pod.CurrentState.Host+"/"+pod.CurrentState.HostIP)
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(pod.Labels)) fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(pod.Labels))
fmt.Fprintf(out, "Status:\t%s\n", string(pod.CurrentState.Status)) 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 return nil
}) })
} }
func describeReplicationController(w io.Writer, c client.Interface, id string) (string, error) { // ReplicationControllerDescriber generates information about a replication controller
// TODO this needs proper namespace support // and the pods it has created.
controller, err := c.ReplicationControllers(api.NamespaceDefault).Get(id) 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 { if err != nil {
return "", err 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 { if err != nil {
return "", err 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) { // ServiceDescriber generates information about a service.
service, err := c.Services(api.NamespaceDefault).Get(id) 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 { if err != nil {
return "", err 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) { // MinionDescriber generates information about a minion.
minion, err := getMinion(c, id) 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 { if err != nil {
return "", err 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 // Get all replication controllers whose selectors would match a given set of
// labels. // labels.
// TODO Move this to pkg/client and ideally implement it server-side (instead // TODO Move this to pkg/client and ideally implement it server-side (instead
// of getting all RC's and searching through them manually). // 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. // Get all replication controllers.
// TODO this needs a namespace scope as argument // 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 { if err != nil {
glog.Fatalf("Error getting replication controllers: %v\n", err) glog.Fatalf("Error getting replication controllers: %v\n", err)
} }
@ -171,8 +216,8 @@ func getReplicationControllersForLabels(c client.Interface, labelsToMatch labels
return list return list
} }
func getPodStatusForReplicationController(kubeClient client.Interface, controller *api.ReplicationController) (running, waiting, terminated int, err error) { func getPodStatusForReplicationController(c client.PodInterface, controller *api.ReplicationController) (running, waiting, terminated int, err error) {
rcPods, err := kubeClient.Pods(controller.Namespace).List(labels.SelectorFromSet(controller.DesiredState.ReplicaSelector)) rcPods, err := c.List(labels.SelectorFromSet(controller.DesiredState.ReplicaSelector))
if err != nil { if err != nil {
return return
} }

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -31,8 +31,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/GoogleCloudPlatform/kubernetes/pkg/version"
"gopkg.in/v1/yaml"
) )
var apiVersionToUse = "v1beta1" var apiVersionToUse = "v1beta1"
@ -132,16 +130,6 @@ func promptForString(field string, r io.Reader) string {
return result 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. // TODO Move to labels package.
func formatLabels(labelMap map[string]string) string { func formatLabels(labelMap map[string]string) string {
l := labels.Set(labelMap).String() l := labels.Set(labelMap).String()
@ -158,90 +146,3 @@ func makeImageList(manifest api.ContainerManifest) string {
} }
return strings.Join(images, ",") 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
}

View File

@ -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
}
}
}

View File

@ -34,7 +34,20 @@ import (
"gopkg.in/v1/yaml" "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 var printer ResourcePrinter
switch format { switch format {
case "json": case "json":
@ -60,7 +73,7 @@ func getPrinter(format, templateFile string, noHeaders bool) (ResourcePrinter, e
Template: tmpl, Template: tmpl,
} }
default: default:
printer = NewHumanReadablePrinter(noHeaders) printer = defaultPrinter
} }
return printer, nil return printer, nil
} }

View File

@ -18,12 +18,13 @@ package kubectl
import ( import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
) )
// RESTModifier provides methods for mutating a known or unknown // RESTHelper provides methods for retrieving or mutating a RESTful
// RESTful resource. // resource.
type RESTModifier struct { type RESTHelper struct {
Resource string Resource string
// A RESTClient capable of mutating this resource // A RESTClient capable of mutating this resource
RESTClient RESTClient RESTClient RESTClient
@ -34,9 +35,9 @@ type RESTModifier struct {
Versioner runtime.ResourceVersioner Versioner runtime.ResourceVersioner
} }
// NewRESTModifier creates a RESTModifier from a RESTMapping // NewRESTHelper creates a RESTHelper from a ResourceMapping
func NewRESTModifier(client RESTClient, mapping *meta.RESTMapping) *RESTModifier { func NewRESTHelper(client RESTClient, mapping *meta.RESTMapping) *RESTHelper {
return &RESTModifier{ return &RESTHelper{
RESTClient: client, RESTClient: client,
Resource: mapping.Resource, Resource: mapping.Resource,
Codec: mapping.Codec, Codec: mapping.Codec,
@ -44,35 +45,39 @@ func NewRESTModifier(client RESTClient, mapping *meta.RESTMapping) *RESTModifier
} }
} }
func (m *RESTModifier) Delete(namespace, name string) error { func (m *RESTHelper) Get(namespace, name string, selector labels.Selector) (runtime.Object, error) {
return m.RESTClient.Delete().Path(m.Resource).Path(name).Do().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 { func (m *RESTHelper) Delete(namespace, name string) error {
return m.RESTClient.Post().Path(m.Resource).Body(data).Do().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 c := m.RESTClient
obj, err := m.Codec.Decode(data) obj, err := m.Codec.Decode(data)
if err != nil { if err != nil {
// We don't know how to handle this object, but update it anyway // 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. // Attempt to version the object based on client logic.
version, err := m.Versioner.ResourceVersion(obj) version, err := m.Versioner.ResourceVersion(obj)
if err != nil { if err != nil {
// We don't know how to version this object, so send it to the server as is // 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 { if version == "" && overwrite {
// Retrieve the current version of the object to overwrite the server object // Retrieve the current version of the object to overwrite the server object
serverObj, err := c.Get().Path(m.Resource).Path(name).Do().Get() serverObj, err := c.Get().Path(m.Resource).Path(name).Do().Get()
if err != nil { if err != nil {
// The object does not exist, but we want it to be created // 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) serverVersion, err := m.Versioner.ResourceVersion(serverObj)
if err != nil { if err != nil {
@ -88,5 +93,9 @@ func (m *RESTModifier) Update(namespace, name string, overwrite bool, data []byt
data = newData 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()
} }

View File

@ -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))
}
}
}