diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index dd194d14e64..96d10ac6ec9 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -232,6 +232,8 @@ _kubectl_get() flags+=("--all-namespaces") flags+=("--help") flags+=("-h") + flags+=("--label-columns=") + two_word_flags+=("-L") flags+=("--no-headers") flags+=("--output=") two_word_flags+=("-o") diff --git a/docs/kubectl_get.md b/docs/kubectl_get.md index e8b0726cf44..df7df373611 100644 --- a/docs/kubectl_get.md +++ b/docs/kubectl_get.md @@ -46,6 +46,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 ``` --all-namespaces=false: If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help=false: help for get + -L, --label-columns=[]: Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag statements like -L label1 -L label2... --no-headers=false: When using the default output, don't print headers. -o, --output="": Output format. One of: json|yaml|template|templatefile. --output-version="": Output the formatted object with the given version (default api-version). @@ -87,6 +88,6 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-05 21:08:36.511279339 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-21 22:41:03.746552518 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_get.md?pixel)]() diff --git a/docs/man/man1/kubectl-get.1 b/docs/man/man1/kubectl-get.1 index e71f884a218..dd250f11e12 100644 --- a/docs/man/man1/kubectl-get.1 +++ b/docs/man/man1/kubectl-get.1 @@ -35,6 +35,10 @@ of the \-\-template flag, you can filter the attributes of the fetched resource( \fB\-h\fP, \fB\-\-help\fP=false help for get +.PP +\fB\-L\fP, \fB\-\-label\-columns\fP=[] + Accepts a comma separated list of labels that are going to be presented as columns. Names are case\-sensitive. You can also use multiple flag statements like \-L label1 \-L label2... + .PP \fB\-\-no\-headers\fP=false When using the default output, don't print headers. diff --git a/pkg/kubectl/bash_comp_utils.go b/pkg/kubectl/bash_comp_utils.go index fe2870f8e7e..dcd1b85b38c 100644 --- a/pkg/kubectl/bash_comp_utils.go +++ b/pkg/kubectl/bash_comp_utils.go @@ -15,6 +15,7 @@ limitations under the License. */ // A set of common functions needed by cmd/kubectl and pkg/kubectl packages. + package kubectl import ( @@ -38,3 +39,15 @@ func AddJsonFilenameFlag(cmd *cobra.Command, value *util.StringList, usage strin } cmd.Flags().AddFlag(flag) } + +// AddLabelsToColumnsFlag added a user flag to print resource labels into columns. Currently used in kubectl get command +func AddLabelsToColumnsFlag(cmd *cobra.Command, value *util.StringList, usage string) { + flag := &pflag.Flag{ + Name: "label-columns", + Shorthand: "L", + Usage: usage, + Value: value, + DefValue: value.String(), + } + cmd.Flags().AddFlag(flag) +} diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 5815b250176..7991a57b776 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -140,7 +140,7 @@ func NewTestFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { Describer: func(*meta.RESTMapping) (kubectl.Describer, error) { return t.Describer, t.Err }, - Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, columnLabels []string) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, Validator: func() (validation.Schema, error) { @@ -194,7 +194,7 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { Describer: func(*meta.RESTMapping) (kubectl.Describer, error) { return t.Describer, t.Err }, - Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, columnLabels []string) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, Validator: func() (validation.Schema, error) { @@ -243,7 +243,7 @@ func stringBody(body string) io.ReadCloser { func ExamplePrintReplicationController() { f, tf, codec := NewAPIFactory() - tf.Printer = kubectl.NewHumanReadablePrinter(false, false) + tf.Printer = kubectl.NewHumanReadablePrinter(false, false, []string{}) tf.Client = &client.FakeRESTClient{ Codec: codec, Client: nil, diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 8cfdc54749c..8cd8f506aaa 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -24,6 +24,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/spf13/cobra" @@ -61,7 +62,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7` // NewCmdGet creates a command object for the generic "get" action, which // retrieves one or more resources from a server. func NewCmdGet(f *cmdutil.Factory, out io.Writer) *cobra.Command { - p := kubectl.NewHumanReadablePrinter(false, false) + p := kubectl.NewHumanReadablePrinter(false, false, []string{}) validArgs := p.HandledResources() cmd := &cobra.Command{ @@ -80,6 +81,7 @@ func NewCmdGet(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().BoolP("watch", "w", false, "After listing/getting the requested object, watch for changes.") cmd.Flags().Bool("watch-only", false, "Watch for changes to the requested object(s), without listing/getting first.") cmd.Flags().Bool("all-namespaces", false, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + kubectl.AddLabelsToColumnsFlag(cmd, &util.StringList{}, "Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag statements like -L label1 -L label2...") return cmd } diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index ae799cfd1aa..831e448995b 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -64,7 +64,7 @@ type Factory struct { // Returns a Describer for displaying the specified RESTMapping type or an error. Describer func(mapping *meta.RESTMapping) (kubectl.Describer, error) // Returns a Printer for formatting objects of the given type or an error. - Printer func(mapping *meta.RESTMapping, noHeaders, withNamespace bool) (kubectl.ResourcePrinter, error) + Printer func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, columnLabels []string) (kubectl.ResourcePrinter, error) // Returns a Scaler for changing the size of the specified RESTMapping type or an error Scaler func(mapping *meta.RESTMapping) (kubectl.Scaler, error) // Returns a Reaper for gracefully shutting down resources. @@ -140,8 +140,8 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return describer, nil }, - Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool) (kubectl.ResourcePrinter, error) { - return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace), nil + Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, columnLabels []string) (kubectl.ResourcePrinter, error) { + return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace, columnLabels), nil }, PodSelectorForObject: func(object runtime.Object) (string, error) { // TODO: replace with a swagger schema based approach (identify pod selector via schema introspection) @@ -378,7 +378,7 @@ func (f *Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMappin } printer = kubectl.NewVersionedPrinter(printer, mapping.ObjectConvertor, version, mapping.APIVersion) } else { - printer, err = f.Printer(mapping, GetFlagBool(cmd, "no-headers"), withNamespace) + printer, err = f.Printer(mapping, GetFlagBool(cmd, "no-headers"), withNamespace, GetFlagStringList(cmd, "label-columns")) if err != nil { return nil, err } diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 2d3b59a27e0..b4e9a6aa8c2 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -35,6 +35,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" "github.com/evanphx/json-patch" @@ -250,6 +251,15 @@ func GetFlagString(cmd *cobra.Command, flag string) string { return f.Value.String() } +// GetFlagStringList can be used to accept multiple argument with flag repetition (e.g. -f arg1 -f arg2 ...) +func GetFlagStringList(cmd *cobra.Command, flag string) util.StringList { + f := cmd.Flags().Lookup(flag) + if f == nil { + return util.StringList{} + } + return *f.Value.(*util.StringList) +} + func GetFlagBool(cmd *cobra.Command, flag string) bool { f := getFlag(cmd, flag) result, err := strconv.ParseBool(f.Value.String()) diff --git a/pkg/kubectl/cmd/util/printing.go b/pkg/kubectl/cmd/util/printing.go index 4dd0bd226f5..ab646fbe616 100644 --- a/pkg/kubectl/cmd/util/printing.go +++ b/pkg/kubectl/cmd/util/printing.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" ) +// AddPrinterFlags adds printing related flags to a command (e.g. output format, no headers, template path) func AddPrinterFlags(cmd *cobra.Command) { cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|template|templatefile.") cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).") diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index c51c70981d8..606e17979c9 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -180,15 +180,17 @@ type HumanReadablePrinter struct { handlerMap map[reflect.Type]*handlerEntry noHeaders bool withNamespace bool + columnLabels []string lastType reflect.Type } // NewHumanReadablePrinter creates a HumanReadablePrinter. -func NewHumanReadablePrinter(noHeaders, withNamespace bool) *HumanReadablePrinter { +func NewHumanReadablePrinter(noHeaders, withNamespace bool, columnLabels []string) *HumanReadablePrinter { printer := &HumanReadablePrinter{ handlerMap: make(map[reflect.Type]*handlerEntry), noHeaders: noHeaders, withNamespace: withNamespace, + columnLabels: columnLabels, } printer.addDefaultHandlers() return printer @@ -215,17 +217,18 @@ func (h *HumanReadablePrinter) Handler(columns []string, printFunc interface{}) func (h *HumanReadablePrinter) validatePrintHandlerFunc(printFunc reflect.Value) error { if printFunc.Kind() != reflect.Func { - return fmt.Errorf("invalid print handler. %#v is not a function.", printFunc) + return fmt.Errorf("invalid print handler. %#v is not a function", printFunc) } funcType := printFunc.Type() - if funcType.NumIn() != 3 || funcType.NumOut() != 1 { + if funcType.NumIn() != 4 || funcType.NumOut() != 1 { return fmt.Errorf("invalid print handler." + "Must accept 3 parameters and return 1 value.") } if funcType.In(1) != reflect.TypeOf((*io.Writer)(nil)).Elem() || + funcType.In(3) != reflect.TypeOf((*[]string)(nil)).Elem() || funcType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { return fmt.Errorf("invalid print handler. The expected signature is: "+ - "func handler(obj %v, w io.Writer, withNamespace bool) error", funcType.In(0)) + "func handler(obj %v, w io.Writer, withNamespace bool, columnLabels []string) error", funcType.In(0)) } return nil } @@ -368,7 +371,7 @@ func translateTimestamp(timestamp util.Time) string { return shortHumanDuration(time.Now().Sub(timestamp.Time)) } -func printPod(pod *api.Pod, w io.Writer, withNamespace bool) error { +func printPod(pod *api.Pod, w io.Writer, withNamespace bool, columnLabels []string) error { name := pod.Name if withNamespace { name = types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}.String() @@ -402,29 +405,30 @@ func printPod(pod *api.Pod, w io.Writer, withNamespace bool) error { } } - _, err := fmt.Fprintf(w, "%s\t%d/%d\t%s\t%d\t%s\n", + if _, err := fmt.Fprintf(w, "%s\t%d/%d\t%s\t%d\t%s", name, readyContainers, totalContainers, reason, restarts, - translateTimestamp(pod.CreationTimestamp)) - if err != nil { + translateTimestamp(pod.CreationTimestamp), + ); err != nil { return err } - return nil + _, err := fmt.Fprint(w, appendLabels(pod.Labels, columnLabels)) + return err } -func printPodList(podList *api.PodList, w io.Writer, withNamespace bool) error { +func printPodList(podList *api.PodList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, pod := range podList.Items { - if err := printPod(&pod, w, withNamespace); err != nil { + if err := printPod(&pod, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool) error { +func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{pod.Namespace, pod.Name}.String() @@ -437,15 +441,19 @@ func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool) err if len(containers) > 0 { firstContainer, containers = containers[0], containers[1:] } - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", + + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s", name, firstContainer.Name, firstContainer.Image, formatLabels(pod.Template.Labels), - ) - if err != nil { + ); err != nil { return err } + if _, err := fmt.Fprint(w, appendLabels(pod.Labels, columnLabels)); err != nil { + return err + } + // Lay out all the other containers on separate lines. for _, container := range containers { _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "", container.Name, container.Image, "") @@ -456,16 +464,16 @@ func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool) err return nil } -func printPodTemplateList(podList *api.PodTemplateList, w io.Writer, withNamespace bool) error { +func printPodTemplateList(podList *api.PodTemplateList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, pod := range podList.Items { - if err := printPodTemplate(&pod, w, withNamespace); err != nil { + if err := printPodTemplate(&pod, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printReplicationController(controller *api.ReplicationController, w io.Writer, withNamespace bool) error { +func printReplicationController(controller *api.ReplicationController, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{controller.Namespace, controller.Name}.String() @@ -478,15 +486,20 @@ func printReplicationController(controller *api.ReplicationController, w io.Writ if len(containers) > 0 { firstContainer, containers = containers[0], containers[1:] } - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", + + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d", name, firstContainer.Name, firstContainer.Image, formatLabels(controller.Spec.Selector), - controller.Spec.Replicas) - if err != nil { + controller.Spec.Replicas, + ); err != nil { return err } + if _, err := fmt.Fprint(w, appendLabels(controller.Labels, columnLabels)); err != nil { + return err + } + // Lay out all the other containers on separate lines. for _, container := range containers { _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", "", container.Name, container.Image, "", "") @@ -497,16 +510,16 @@ func printReplicationController(controller *api.ReplicationController, w io.Writ return nil } -func printReplicationControllerList(list *api.ReplicationControllerList, w io.Writer, withNamespace bool) error { +func printReplicationControllerList(list *api.ReplicationControllerList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, controller := range list.Items { - if err := printReplicationController(&controller, w, withNamespace); err != nil { + if err := printReplicationController(&controller, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printService(svc *api.Service, w io.Writer, withNamespace bool) error { +func printService(svc *api.Service, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{svc.Namespace, svc.Name}.String() @@ -523,10 +536,13 @@ func printService(svc *api.Service, w io.Writer, withNamespace bool) error { } } - if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", name, formatLabels(svc.Labels), + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s", name, formatLabels(svc.Labels), formatLabels(svc.Spec.Selector), ips[0], svc.Spec.Ports[0].Port, svc.Spec.Ports[0].Protocol); err != nil { return err } + if _, err := fmt.Fprint(w, appendLabels(svc.Labels, columnLabels)); err != nil { + return err + } count := len(svc.Spec.Ports) if len(ips) > count { @@ -550,50 +566,57 @@ func printService(svc *api.Service, w io.Writer, withNamespace bool) error { return nil } -func printServiceList(list *api.ServiceList, w io.Writer, withNamespace bool) error { +func printServiceList(list *api.ServiceList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, svc := range list.Items { - if err := printService(&svc, w, withNamespace); err != nil { + if err := printService(&svc, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printEndpoints(endpoints *api.Endpoints, w io.Writer, withNamespace bool) error { +func printEndpoints(endpoints *api.Endpoints, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{endpoints.Namespace, endpoints.Name}.String() } else { name = endpoints.Name } - _, err := fmt.Fprintf(w, "%s\t%s\n", name, formatEndpoints(endpoints, nil)) + + if _, err := fmt.Fprintf(w, "%s\t%s", name, formatEndpoints(endpoints, nil)); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(endpoints.Labels, columnLabels)) return err } -func printEndpointsList(list *api.EndpointsList, w io.Writer, withNamespace bool) error { +func printEndpointsList(list *api.EndpointsList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, item := range list.Items { - if err := printEndpoints(&item, w, withNamespace); err != nil { + if err := printEndpoints(&item, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printNamespace(item *api.Namespace, w io.Writer, withNamespace bool) error { - _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", item.Name, formatLabels(item.Labels), item.Status.Phase) +func printNamespace(item *api.Namespace, w io.Writer, withNamespace bool, columnLabels []string) error { + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", item.Name, formatLabels(item.Labels), item.Status.Phase); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(item.Labels, columnLabels)) return err } -func printNamespaceList(list *api.NamespaceList, w io.Writer, withNamespace bool) error { +func printNamespaceList(list *api.NamespaceList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, item := range list.Items { - if err := printNamespace(&item, w, withNamespace); err != nil { + if err := printNamespace(&item, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printSecret(item *api.Secret, w io.Writer, withNamespace bool) error { +func printSecret(item *api.Secret, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{item.Namespace, item.Name}.String() @@ -601,13 +624,16 @@ func printSecret(item *api.Secret, w io.Writer, withNamespace bool) error { name = item.Name } - _, err := fmt.Fprintf(w, "%s\t%s\t%v\n", name, item.Type, len(item.Data)) + if _, err := fmt.Fprintf(w, "%s\t%s\t%v", name, item.Type, len(item.Data)); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(item.Labels, columnLabels)) return err } -func printSecretList(list *api.SecretList, w io.Writer, withNamespace bool) error { +func printSecretList(list *api.SecretList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, item := range list.Items { - if err := printSecret(&item, w, withNamespace); err != nil { + if err := printSecret(&item, w, withNamespace, columnLabels); err != nil { return err } } @@ -615,7 +641,7 @@ func printSecretList(list *api.SecretList, w io.Writer, withNamespace bool) erro return nil } -func printServiceAccount(item *api.ServiceAccount, w io.Writer, withNamespace bool) error { +func printServiceAccount(item *api.ServiceAccount, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{item.Namespace, item.Name}.String() @@ -623,13 +649,16 @@ func printServiceAccount(item *api.ServiceAccount, w io.Writer, withNamespace bo name = item.Name } - _, err := fmt.Fprintf(w, "%s\t%d\n", name, len(item.Secrets)) + if _, err := fmt.Fprintf(w, "%s\t%d", name, len(item.Secrets)); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(item.Labels, columnLabels)) return err } -func printServiceAccountList(list *api.ServiceAccountList, w io.Writer, withNamespace bool) error { +func printServiceAccountList(list *api.ServiceAccountList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, item := range list.Items { - if err := printServiceAccount(&item, w, withNamespace); err != nil { + if err := printServiceAccount(&item, w, withNamespace, columnLabels); err != nil { return err } } @@ -637,7 +666,7 @@ func printServiceAccountList(list *api.ServiceAccountList, w io.Writer, withName return nil } -func printNode(node *api.Node, w io.Writer, withNamespace bool) error { +func printNode(node *api.Node, w io.Writer, withNamespace bool, columnLabels []string) error { conditionMap := make(map[api.NodeConditionType]*api.NodeCondition) NodeAllConditions := []api.NodeConditionType{api.NodeReady} for i := range node.Status.Conditions { @@ -660,20 +689,24 @@ func printNode(node *api.Node, w io.Writer, withNamespace bool) error { if node.Spec.Unschedulable { status = append(status, "SchedulingDisabled") } - _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", node.Name, formatLabels(node.Labels), strings.Join(status, ",")) + + if _, err := fmt.Fprintf(w, "%s\t%s\t%s", node.Name, formatLabels(node.Labels), strings.Join(status, ",")); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(node.Labels, columnLabels)) return err } -func printNodeList(list *api.NodeList, w io.Writer, withNamespace bool) error { +func printNodeList(list *api.NodeList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, node := range list.Items { - if err := printNode(&node, w, withNamespace); err != nil { + if err := printNode(&node, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printPersistentVolume(pv *api.PersistentVolume, w io.Writer, withNamespace bool) error { +func printPersistentVolume(pv *api.PersistentVolume, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{pv.Namespace, pv.Name}.String() @@ -693,20 +726,23 @@ func printPersistentVolume(pv *api.PersistentVolume, w io.Writer, withNamespace aQty := pv.Spec.Capacity[api.ResourceStorage] aSize := aQty.Value() - _, err := fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\t%s\n", name, formatLabels(pv.Labels), aSize, modesStr, pv.Status.Phase, claimRefUID, pv.Status.Reason) + if _, err := fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\t%s", name, formatLabels(pv.Labels), aSize, modesStr, pv.Status.Phase, claimRefUID, pv.Status.Reason); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(pv.Labels, columnLabels)) return err } -func printPersistentVolumeList(list *api.PersistentVolumeList, w io.Writer, withNamespace bool) error { +func printPersistentVolumeList(list *api.PersistentVolumeList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, pv := range list.Items { - if err := printPersistentVolume(&pv, w, withNamespace); err != nil { + if err := printPersistentVolume(&pv, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer, withNamespace bool) error { +func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{pvc.Namespace, pvc.Name}.String() @@ -714,22 +750,25 @@ func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer, wit name = pvc.Name } - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, pvc.Labels, pvc.Status.Phase, pvc.Spec.VolumeName) + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s", name, pvc.Labels, pvc.Status.Phase, pvc.Spec.VolumeName); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(pvc.Labels, columnLabels)) return err } -func printPersistentVolumeClaimList(list *api.PersistentVolumeClaimList, w io.Writer, withNamespace bool) error { +func printPersistentVolumeClaimList(list *api.PersistentVolumeClaimList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, psd := range list.Items { - if err := printPersistentVolumeClaim(&psd, w, withNamespace); err != nil { + if err := printPersistentVolumeClaim(&psd, w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printEvent(event *api.Event, w io.Writer, withNamespace bool) error { - _, err := fmt.Fprintf( - w, "%s\t%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\n", +func printEvent(event *api.Event, w io.Writer, withNamespace bool, columnLabels []string) error { + if _, err := fmt.Fprintf( + w, "%s\t%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s", event.FirstTimestamp.Time.Format(time.RFC1123Z), event.LastTimestamp.Time.Format(time.RFC1123Z), event.Count, @@ -739,22 +778,25 @@ func printEvent(event *api.Event, w io.Writer, withNamespace bool) error { event.Reason, event.Source, event.Message, - ) + ); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(event.Labels, columnLabels)) return err } // Sorts and prints the EventList in a human-friendly format. -func printEventList(list *api.EventList, w io.Writer, withNamespace bool) error { +func printEventList(list *api.EventList, w io.Writer, withNamespace bool, columnLabels []string) error { sort.Sort(SortableEvents(list.Items)) for i := range list.Items { - if err := printEvent(&list.Items[i], w, withNamespace); err != nil { + if err := printEvent(&list.Items[i], w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printLimitRange(limitRange *api.LimitRange, w io.Writer, withNamespace bool) error { +func printLimitRange(limitRange *api.LimitRange, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{limitRange.Namespace, limitRange.Name}.String() @@ -762,24 +804,24 @@ func printLimitRange(limitRange *api.LimitRange, w io.Writer, withNamespace bool name = limitRange.Name } - _, err := fmt.Fprintf( - w, "%s\n", - name, - ) + if _, err := fmt.Fprintf(w, "%s", name); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(limitRange.Labels, columnLabels)) return err } // Prints the LimitRangeList in a human-friendly format. -func printLimitRangeList(list *api.LimitRangeList, w io.Writer, withNamespace bool) error { +func printLimitRangeList(list *api.LimitRangeList, w io.Writer, withNamespace bool, columnLabels []string) error { for i := range list.Items { - if err := printLimitRange(&list.Items[i], w, withNamespace); err != nil { + if err := printLimitRange(&list.Items[i], w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, withNamespace bool) error { +func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, withNamespace bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{resourceQuota.Namespace, resourceQuota.Name}.String() @@ -787,24 +829,24 @@ func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, withNames name = resourceQuota.Name } - _, err := fmt.Fprintf( - w, "%s\n", - name, - ) + if _, err := fmt.Fprintf(w, "%s", name); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(resourceQuota.Labels, columnLabels)) return err } // Prints the ResourceQuotaList in a human-friendly format. -func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer, withNamespace bool) error { +func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer, withNamespace bool, columnLabels []string) error { for i := range list.Items { - if err := printResourceQuota(&list.Items[i], w, withNamespace); err != nil { + if err := printResourceQuota(&list.Items[i], w, withNamespace, columnLabels); err != nil { return err } } return nil } -func printComponentStatus(item *api.ComponentStatus, w io.Writer, withNamespace bool) error { +func printComponentStatus(item *api.ComponentStatus, w io.Writer, withNamespace bool, columnLabels []string) error { status := "Unknown" message := "" error := "" @@ -820,13 +862,17 @@ func printComponentStatus(item *api.ComponentStatus, w io.Writer, withNamespace break } } - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", item.Name, status, message, error) + + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s", item.Name, status, message, error); err != nil { + return err + } + _, err := fmt.Fprint(w, appendLabels(item.Labels, columnLabels)) return err } -func printComponentStatusList(list *api.ComponentStatusList, w io.Writer, withNamespace bool) error { +func printComponentStatusList(list *api.ComponentStatusList, w io.Writer, withNamespace bool, columnLabels []string) error { for _, item := range list.Items { - if err := printComponentStatus(&item, w, withNamespace); err != nil { + if err := printComponentStatus(&item, w, withNamespace, columnLabels); err != nil { return err } } @@ -834,6 +880,31 @@ func printComponentStatusList(list *api.ComponentStatusList, w io.Writer, withNa return nil } +func appendLabels(itemLabels map[string]string, columnLabels []string) string { + var buffer bytes.Buffer + + for _, cl := range columnLabels { + buffer.WriteString(fmt.Sprint("\t")) + if il, ok := itemLabels[cl]; ok { + buffer.WriteString(fmt.Sprint(il)) + } else { + buffer.WriteString("") + } + } + buffer.WriteString("\n") + + return buffer.String() +} + +func formatLabelHeaders(columnLabels []string) []string { + formHead := make([]string, len(columnLabels)) + for i, l := range columnLabels { + p := strings.Split(l, "/") + formHead[i] = strings.ToUpper((p[len(p)-1])) + } + return formHead +} + // PrintObj prints the obj in a human-friendly format according to the type of the obj. func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error { w := tabwriter.NewWriter(output, 10, 4, 3, ' ', 0) @@ -841,10 +912,11 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er t := reflect.TypeOf(obj) if handler := h.handlerMap[t]; handler != nil { if !h.noHeaders && t != h.lastType { - h.printHeader(handler.columns, w) + headers := append(handler.columns, formatLabelHeaders(h.columnLabels)...) + h.printHeader(headers, w) h.lastType = t } - args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w), reflect.ValueOf(h.withNamespace)} + args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w), reflect.ValueOf(h.withNamespace), reflect.ValueOf(h.columnLabels)} resultValue := handler.printFunc.Call(args)[0] if resultValue.IsNil() { return nil diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go index 07f8186fdbb..f578709f203 100644 --- a/pkg/kubectl/resource_printer_test.go +++ b/pkg/kubectl/resource_printer_test.go @@ -237,18 +237,18 @@ type TestUnknownType struct{} func (*TestUnknownType) IsAnAPIObject() {} -func PrintCustomType(obj *TestPrintType, w io.Writer, withNamespace bool) error { +func PrintCustomType(obj *TestPrintType, w io.Writer, withNamespace bool, columnLabels []string) error { _, err := fmt.Fprintf(w, "%s", obj.Data) return err } -func ErrorPrintHandler(obj *TestPrintType, w io.Writer, withNamespace bool) error { +func ErrorPrintHandler(obj *TestPrintType, w io.Writer, withNamespace bool, columnLabels []string) error { return fmt.Errorf("ErrorPrintHandler error") } func TestCustomTypePrinting(t *testing.T) { columns := []string{"Data"} - printer := NewHumanReadablePrinter(false, false) + printer := NewHumanReadablePrinter(false, false, []string{}) printer.Handler(columns, PrintCustomType) obj := TestPrintType{"test object"} @@ -265,7 +265,7 @@ func TestCustomTypePrinting(t *testing.T) { func TestPrintHandlerError(t *testing.T) { columns := []string{"Data"} - printer := NewHumanReadablePrinter(false, false) + printer := NewHumanReadablePrinter(false, false, []string{}) printer.Handler(columns, ErrorPrintHandler) obj := TestPrintType{"test object"} buffer := &bytes.Buffer{} @@ -276,7 +276,7 @@ func TestPrintHandlerError(t *testing.T) { } func TestUnknownTypePrinting(t *testing.T) { - printer := NewHumanReadablePrinter(false, false) + printer := NewHumanReadablePrinter(false, false, []string{}) buffer := &bytes.Buffer{} err := printer.PrintObj(&TestUnknownType{}, buffer) if err == nil { @@ -452,8 +452,8 @@ func TestPrinters(t *testing.T) { t.Fatal(err) } printers := map[string]ResourcePrinter{ - "humanReadable": NewHumanReadablePrinter(true, false), - "humanReadableHeaders": NewHumanReadablePrinter(false, false), + "humanReadable": NewHumanReadablePrinter(true, false, []string{}), + "humanReadableHeaders": NewHumanReadablePrinter(false, false, []string{}), "json": &JSONPrinter{}, "yaml": &YAMLPrinter{}, "template": templatePrinter, @@ -490,7 +490,7 @@ func TestPrinters(t *testing.T) { func TestPrintEventsResultSorted(t *testing.T) { // Arrange - printer := NewHumanReadablePrinter(false /* noHeaders */, false) + printer := NewHumanReadablePrinter(false /* noHeaders */, false, []string{}) obj := api.EventList{ Items: []api.Event{ @@ -531,7 +531,7 @@ func TestPrintEventsResultSorted(t *testing.T) { } func TestPrintMinionStatus(t *testing.T) { - printer := NewHumanReadablePrinter(false, false) + printer := NewHumanReadablePrinter(false, false, []string{}) table := []struct { minion api.Node status string @@ -739,7 +739,7 @@ func TestPrintHumanReadableService(t *testing.T) { for _, svc := range tests { buff := bytes.Buffer{} - printService(&svc, &buff, false) + printService(&svc, &buff, false, []string{}) output := string(buff.Bytes()) ip := svc.Spec.ClusterIP if !strings.Contains(output, ip) { @@ -921,7 +921,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { }, } - printer := NewHumanReadablePrinter(false, false) + printer := NewHumanReadablePrinter(false, false, []string{}) for _, test := range table { buffer := &bytes.Buffer{} err := printer.PrintObj(test.obj, buffer) @@ -934,7 +934,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { } } - printer = NewHumanReadablePrinter(false, true) + printer = NewHumanReadablePrinter(false, true, []string{}) for _, test := range table { buffer := &bytes.Buffer{} err := printer.PrintObj(test.obj, buffer) @@ -1035,7 +1035,7 @@ func TestPrintPod(t *testing.T) { buf := bytes.NewBuffer([]byte{}) for _, test := range tests { - printPod(&test.pod, buf, false) + printPod(&test.pod, buf, false, []string{}) // We ignore time if !strings.HasPrefix(buf.String(), test.expect) { t.Fatalf("Expected: %s, got: %s", test.expect, buf.String()) @@ -1043,3 +1043,63 @@ func TestPrintPod(t *testing.T) { buf.Reset() } } + +func TestPrintPodWithLabels(t *testing.T) { + tests := []struct { + pod api.Pod + labelColumns []string + startsWith string + endsWith string + }{ + { + // Test name, num of containers, restarts, container ready status + api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "test1", + Labels: map[string]string{"col1": "asd", "COL2": "zxc"}, + }, + Spec: api.PodSpec{Containers: make([]api.Container, 2)}, + Status: api.PodStatus{ + Phase: "podPhase", + ContainerStatuses: []api.ContainerStatus{ + {Ready: true, RestartCount: 3, State: api.ContainerState{Running: &api.ContainerStateRunning{}}}, + {RestartCount: 3}, + }, + }, + }, + []string{"col1", "COL2"}, + "test1\t1/2\tpodPhase\t6\t", + "\tasd\tzxc\n", + }, + { + // Test name, num of containers, restarts, container ready status + api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "test1", + Labels: map[string]string{"col1": "asd", "COL2": "zxc"}, + }, + Spec: api.PodSpec{Containers: make([]api.Container, 2)}, + Status: api.PodStatus{ + Phase: "podPhase", + ContainerStatuses: []api.ContainerStatus{ + {Ready: true, RestartCount: 3, State: api.ContainerState{Running: &api.ContainerStateRunning{}}}, + {RestartCount: 3}, + }, + }, + }, + []string{}, + "test1\t1/2\tpodPhase\t6\t", + "\n", + }, + } + + buf := bytes.NewBuffer([]byte{}) + for _, test := range tests { + printPod(&test.pod, buf, false, test.labelColumns) + // We ignore time + if !strings.HasPrefix(buf.String(), test.startsWith) || !strings.HasSuffix(buf.String(), test.endsWith) { + t.Fatalf("Expected to start with: %s and end with: %s, but got: %s", test.startsWith, test.endsWith, buf.String()) + } + buf.Reset() + } +}