diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 41b5be643e8..ab7c0d872d7 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -19,6 +19,7 @@ package kubectl import ( "fmt" "io" + "reflect" "sort" "strings" "time" @@ -30,11 +31,32 @@ import ( ) // Describer generates output for the named resource or an error -// if the output could not be generated. +// if the output could not be generated. Implementors typically +// abstract the retrieval of the named object from a remote server. type Describer interface { Describe(namespace, name string) (output string, err error) } +// ObjectDescriber is an interface for displaying arbitrary objects with extra +// information. Use when an object is in hand (on disk, or already retrieved). +// Implementors may ignore the additional information passed on extra, or use it +// by default. ObjectDescribers may return ErrNoDescriber if no suitable describer +// is found. +type ObjectDescriber interface { + DescribeObject(object interface{}, extra ...interface{}) (output string, err error) +} + +// ErrNoDescriber is a structured error indicating the provided object or objects +// cannot be described. +type ErrNoDescriber struct { + Types []string +} + +// Error implements the error interface. +func (e ErrNoDescriber) Error() string { + return fmt.Sprintf("no describer has been defined for %v", e.Types) +} + // Describer returns the default describe functions for each of the standard // Kubernetes types. func DescriberFor(kind string, c *client.Client) (Describer, bool) { @@ -55,6 +77,25 @@ func DescriberFor(kind string, c *client.Client) (Describer, bool) { return nil, false } +// DefaultObjectDescriber can describe the default Kubernetes objects. +var DefaultObjectDescriber ObjectDescriber + +func init() { + d := &Describers{} + err := d.Add( + describeLimitRange, + describeQuota, + describePod, + describeService, + describeReplicationController, + describeNode, + ) + if err != nil { + glog.Fatalf("Cannot register describers: %v", err) + } + DefaultObjectDescriber = d +} + // LimitRangeDescriber generates information about a limit range type LimitRangeDescriber struct { client.Interface @@ -67,7 +108,10 @@ func (d *LimitRangeDescriber) Describe(namespace, name string) (string, error) { if err != nil { return "", err } + return describeLimitRange(limitRange) +} +func describeLimitRange(limitRange *api.LimitRange) (string, error) { return tabbedString(func(out io.Writer) error { fmt.Fprintf(out, "Name:\t%s\n", limitRange.Name) fmt.Fprintf(out, "Type\tResource\tMin\tMax\n") @@ -121,6 +165,10 @@ func (d *ResourceQuotaDescriber) Describe(namespace, name string) (string, error return "", err } + return describeQuota(resourceQuota) +} + +func describeQuota(resourceQuota *api.ResourceQuota) (string, error) { return tabbedString(func(out io.Writer) error { fmt.Fprintf(out, "Name:\t%s\n", resourceQuota.Name) fmt.Fprintf(out, "Resource\tUsed\tHard\n") @@ -172,12 +220,6 @@ func (d *PodDescriber) Describe(namespace, name string) (string, error) { return "", err } - // TODO: remove me when pods are converted - spec := &api.PodSpec{} - if err := api.Scheme.Convert(&pod.Spec, spec); err != nil { - glog.Errorf("Unable to convert pod manifest: %v", err) - } - var events *api.EventList if ref, err := api.GetReference(pod); err != nil { glog.Errorf("Unable to construct reference to '%#v': %v", pod, err) @@ -186,13 +228,22 @@ func (d *PodDescriber) Describe(namespace, name string) (string, error) { events, _ = d.Events(namespace).Search(ref) } + rcs, err := getReplicationControllersForLabels(rc, labels.Set(pod.Labels)) + if err != nil { + return "", err + } + + return describePod(pod, rcs, events) +} + +func describePod(pod *api.Pod, rcs []api.ReplicationController, events *api.EventList) (string, error) { return tabbedString(func(out io.Writer) error { fmt.Fprintf(out, "Name:\t%s\n", pod.Name) - fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(spec)) + fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&pod.Spec)) fmt.Fprintf(out, "Host:\t%s\n", pod.Status.Host+"/"+pod.Status.HostIP) fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(pod.Labels)) fmt.Fprintf(out, "Status:\t%s\n", string(pod.Status.Phase)) - fmt.Fprintf(out, "Replication Controllers:\t%s\n", getReplicationControllersForLabels(rc, labels.Set(pod.Labels))) + fmt.Fprintf(out, "Replication Controllers:\t%s\n", printReplicationControllersByLabels(rcs)) if len(pod.Status.Conditions) > 0 { fmt.Fprint(out, "Conditions:\n Type\tStatus\n") for _, c := range pod.Status.Conditions { @@ -230,9 +281,17 @@ func (d *ReplicationControllerDescriber) Describe(namespace, name string) (strin events, _ := d.Events(namespace).Search(controller) + return describeReplicationController(controller, events, running, waiting, succeeded, failed) +} + +func describeReplicationController(controller *api.ReplicationController, events *api.EventList, running, waiting, succeeded, failed int) (string, error) { return tabbedString(func(out io.Writer) error { fmt.Fprintf(out, "Name:\t%s\n", controller.Name) - fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&controller.Spec.Template.Spec)) + if controller.Spec.Template != nil { + fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&controller.Spec.Template.Spec)) + } else { + fmt.Fprintf(out, "Image(s):\t%s\n", "") + } fmt.Fprintf(out, "Selector:\t%s\n", formatLabels(controller.Spec.Selector)) fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(controller.Labels)) fmt.Fprintf(out, "Replicas:\t%d current / %d desired\n", controller.Status.Replicas, controller.Spec.Replicas) @@ -257,13 +316,16 @@ func (d *ServiceDescriber) Describe(namespace, name string) (string, error) { return "", err } - endpoints, err := d.Endpoints(namespace).Get(name) - if err != nil { - endpoints = &api.Endpoints{} - } - + endpoints, _ := d.Endpoints(namespace).Get(name) events, _ := d.Events(namespace).Search(service) + return describeService(service, endpoints, events) +} + +func describeService(service *api.Service, endpoints *api.Endpoints, events *api.EventList) (string, error) { + if endpoints == nil { + endpoints = &api.Endpoints{} + } return tabbedString(func(out io.Writer) error { fmt.Fprintf(out, "Name:\t%s\n", service.Name) fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(service.Labels)) @@ -309,6 +371,10 @@ func (d *NodeDescriber) Describe(namespace, name string) (string, error) { events, _ := d.Events(namespace).Search(node) + return describeNode(node, pods, events) +} + +func describeNode(node *api.Node, pods []api.Pod, events *api.EventList) (string, error) { return tabbedString(func(out io.Writer) error { fmt.Fprintf(out, "Name:\t%s\n", node.Name) if len(node.Status.Conditions) > 0 { @@ -342,9 +408,6 @@ func (d *NodeDescriber) Describe(namespace, name string) (string, error) { } fmt.Fprintf(out, "Pods:\t(%d in total)\n", len(pods)) for _, pod := range pods { - if pod.Status.Host != name { - continue - } fmt.Fprintf(out, " %s\n", pod.Name) } if events != nil { @@ -377,12 +440,12 @@ func describeEvents(el *api.EventList, w io.Writer) { // 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.ReplicationControllerInterface, labelsToMatch labels.Labels) string { +func getReplicationControllersForLabels(c client.ReplicationControllerInterface, labelsToMatch labels.Labels) ([]api.ReplicationController, error) { // Get all replication controllers. // TODO this needs a namespace scope as argument rcs, err := c.List(labels.Everything()) if err != nil { - glog.Fatalf("Error getting replication controllers: %v\n", err) + return nil, fmt.Errorf("error getting replication controllers: %v", err) } // Find the ones that match labelsToMatch. @@ -393,7 +456,10 @@ func getReplicationControllersForLabels(c client.ReplicationControllerInterface, matchingRCs = append(matchingRCs, controller) } } + return matchingRCs, nil +} +func printReplicationControllersByLabels(matchingRCs []api.ReplicationController) string { // Format the matching RC's into strings. var rcStrings []string for _, controller := range matchingRCs { @@ -426,3 +492,138 @@ func getPodStatusForReplicationController(c client.PodInterface, controller *api } return } + +// newErrNoDescriber creates a new ErrNoDescriber with the names of the provided types. +func newErrNoDescriber(types ...reflect.Type) error { + names := []string{} + for _, t := range types { + names = append(names, t.String()) + } + return ErrNoDescriber{Types: names} +} + +// Describers implements ObjectDescriber against functions registered via Add. Those functions can +// be strongly typed. Types are exactly matched (no conversion or assignable checks). +type Describers struct { + searchFns map[reflect.Type][]typeFunc +} + +// DescribeObject implements ObjectDescriber and will attempt to print the provided object to a string, +// if at least one describer function has been registered with the exact types passed, or if any +// describer can print the exact object in its first argument (the remainder will be provided empty +// values). If no function registered with Add can satisfy the passed objects, an ErrNoDescriber will +// be returned +// TODO: reorder and partial match extra. +func (d *Describers) DescribeObject(exact interface{}, extra ...interface{}) (string, error) { + exactType := reflect.TypeOf(exact) + fns, ok := d.searchFns[exactType] + if !ok { + return "", newErrNoDescriber(exactType) + } + if len(extra) == 0 { + for _, typeFn := range fns { + if len(typeFn.Extra) == 0 { + return typeFn.Describe(exact, extra...) + } + } + typeFn := fns[0] + for _, t := range typeFn.Extra { + v := reflect.New(t).Elem() + extra = append(extra, v.Interface()) + } + return fns[0].Describe(exact, extra...) + } + + types := []reflect.Type{} + for _, obj := range extra { + types = append(types, reflect.TypeOf(obj)) + } + for _, typeFn := range fns { + if typeFn.Matches(types) { + return typeFn.Describe(exact, extra...) + } + } + return "", newErrNoDescriber(append([]reflect.Type{exactType}, types...)...) +} + +// Add adds one or more describer functions to the Describer. The passed function must +// match the signature: +// +// func(...) (string, error) +// +// Any number of arguments may be provided. +func (d *Describers) Add(fns ...interface{}) error { + for _, fn := range fns { + fv := reflect.ValueOf(fn) + ft := fv.Type() + if ft.Kind() != reflect.Func { + return fmt.Errorf("expected func, got: %v", ft) + } + if ft.NumIn() == 0 { + return fmt.Errorf("expected at least one 'in' params, got: %v", ft) + } + if ft.NumOut() != 2 { + return fmt.Errorf("expected two 'out' params - (string, error), got: %v", ft) + } + types := []reflect.Type{} + for i := 0; i < ft.NumIn(); i++ { + types = append(types, ft.In(i)) + } + if ft.Out(0) != reflect.TypeOf(string("")) { + return fmt.Errorf("expected string return, got: %v", ft) + } + var forErrorType error + // This convolution is necessary, otherwise TypeOf picks up on the fact + // that forErrorType is nil. + errorType := reflect.TypeOf(&forErrorType).Elem() + if ft.Out(1) != errorType { + return fmt.Errorf("expected error return, got: %v", ft) + } + + exact := types[0] + extra := types[1:] + if d.searchFns == nil { + d.searchFns = make(map[reflect.Type][]typeFunc) + } + fns := d.searchFns[exact] + fn := typeFunc{Extra: extra, Fn: fv} + fns = append(fns, fn) + d.searchFns[exact] = fns + } + return nil +} + +// typeFunc holds information about a describer function and the types it accepts +type typeFunc struct { + Extra []reflect.Type + Fn reflect.Value +} + +// Matches returns true when the passed types exactly match the Extra list. +// TODO: allow unordered types to be matched and reorderd. +func (fn typeFunc) Matches(types []reflect.Type) bool { + if len(fn.Extra) != len(types) { + return false + } + for i := range types { + if fn.Extra[i] != types[i] { + return false + } + } + return true +} + +// Describe invokes the nested function with the exact number of arguments. +func (fn typeFunc) Describe(exact interface{}, extra ...interface{}) (string, error) { + values := []reflect.Value{reflect.ValueOf(exact)} + for _, obj := range extra { + values = append(values, reflect.ValueOf(obj)) + } + out := fn.Fn.Call(values) + s := out[0].Interface().(string) + var err error + if !out[1].IsNil() { + err = out[1].Interface().(error) + } + return s, err +} diff --git a/pkg/kubectl/describe_test.go b/pkg/kubectl/describe_test.go index 53c10ca5a56..dd9b4680ebc 100644 --- a/pkg/kubectl/describe_test.go +++ b/pkg/kubectl/describe_test.go @@ -17,6 +17,8 @@ limitations under the License. package kubectl import ( + "fmt" + "reflect" "strings" "testing" "time" @@ -104,3 +106,83 @@ func TestPodDescribeResultsSorted(t *testing.T) { } VerifyDatesInOrder(out, "\n" /* rowDelimiter */, "\t" /* columnDelimiter */, t) } + +func TestDescribers(t *testing.T) { + first := &api.Event{} + second := &api.Pod{} + var third *api.Pod + testErr := fmt.Errorf("test") + d := Describers{} + d.Add( + func(e *api.Event, p *api.Pod) (string, error) { + if e != first { + t.Errorf("first argument not equal: %#v", e) + } + if p != second { + t.Errorf("second argument not equal: %#v", p) + } + return "test", testErr + }, + ) + if out, err := d.DescribeObject(first, second); out != "test" || err != testErr { + t.Errorf("unexpected result: %s %v", out, err) + } + + if out, err := d.DescribeObject(first, second, third); out != "" || err == nil { + t.Errorf("unexpected result: %s %v", out, err) + } else { + if noDescriber, ok := err.(ErrNoDescriber); ok { + if !reflect.DeepEqual(noDescriber.Types, []string{"*api.Event", "*api.Pod", "*api.Pod"}) { + t.Errorf("unexpected describer: %v", err) + } + } else { + t.Errorf("unexpected error type: %v", err) + } + } + + d.Add( + func(e *api.Event) (string, error) { + if e != first { + t.Errorf("first argument not equal: %#v", e) + } + return "simpler", testErr + }, + ) + if out, err := d.DescribeObject(first); out != "simpler" || err != testErr { + t.Errorf("unexpected result: %s %v", out, err) + } +} + +func TestDefaultDescribers(t *testing.T) { + out, err := DefaultObjectDescriber.DescribeObject(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(out, "foo") { + t.Errorf("unexpected output: %s", out) + } + + out, err = DefaultObjectDescriber.DescribeObject(&api.Service{ObjectMeta: api.ObjectMeta{Name: "foo"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(out, "foo") { + t.Errorf("unexpected output: %s", out) + } + + out, err = DefaultObjectDescriber.DescribeObject(&api.ReplicationController{ObjectMeta: api.ObjectMeta{Name: "foo"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(out, "foo") { + t.Errorf("unexpected output: %s", out) + } + + out, err = DefaultObjectDescriber.DescribeObject(&api.Node{ObjectMeta: api.ObjectMeta{Name: "foo"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(out, "foo") { + t.Errorf("unexpected output: %s", out) + } +}