Commit b06ef700 authored by Anastasis Andronidis's avatar Anastasis Andronidis

Print resource labels as columns

parent ff0546da
...@@ -232,6 +232,8 @@ _kubectl_get() ...@@ -232,6 +232,8 @@ _kubectl_get()
flags+=("--all-namespaces") flags+=("--all-namespaces")
flags+=("--help") flags+=("--help")
flags+=("-h") flags+=("-h")
flags+=("--label-columns=")
two_word_flags+=("-L")
flags+=("--no-headers") flags+=("--no-headers")
flags+=("--output=") flags+=("--output=")
two_word_flags+=("-o") two_word_flags+=("-o")
......
...@@ -46,6 +46,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 ...@@ -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. --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 -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. --no-headers=false: When using the default output, don't print headers.
-o, --output="": Output format. One of: json|yaml|template|templatefile. -o, --output="": Output format. One of: json|yaml|template|templatefile.
--output-version="": Output the formatted object with the given version (default api-version). --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 ...@@ -87,6 +88,6 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7
### SEE ALSO ### SEE ALSO
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager * [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)]() [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_get.md?pixel)]()
...@@ -36,6 +36,10 @@ of the \-\-template flag, you can filter the attributes of the fetched resource( ...@@ -36,6 +36,10 @@ of the \-\-template flag, you can filter the attributes of the fetched resource(
help for get help for get
.PP .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 \fB\-\-no\-headers\fP=false
When using the default output, don't print headers. When using the default output, don't print headers.
......
...@@ -15,6 +15,7 @@ limitations under the License. ...@@ -15,6 +15,7 @@ limitations under the License.
*/ */
// A set of common functions needed by cmd/kubectl and pkg/kubectl packages. // A set of common functions needed by cmd/kubectl and pkg/kubectl packages.
package kubectl package kubectl
import ( import (
...@@ -38,3 +39,15 @@ func AddJsonFilenameFlag(cmd *cobra.Command, value *util.StringList, usage strin ...@@ -38,3 +39,15 @@ func AddJsonFilenameFlag(cmd *cobra.Command, value *util.StringList, usage strin
} }
cmd.Flags().AddFlag(flag) 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)
}
...@@ -140,7 +140,7 @@ func NewTestFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { ...@@ -140,7 +140,7 @@ func NewTestFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) {
Describer: func(*meta.RESTMapping) (kubectl.Describer, error) { Describer: func(*meta.RESTMapping) (kubectl.Describer, error) {
return t.Describer, t.Err 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 return t.Printer, t.Err
}, },
Validator: func() (validation.Schema, error) { Validator: func() (validation.Schema, error) {
...@@ -194,7 +194,7 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) { ...@@ -194,7 +194,7 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) {
Describer: func(*meta.RESTMapping) (kubectl.Describer, error) { Describer: func(*meta.RESTMapping) (kubectl.Describer, error) {
return t.Describer, t.Err 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 return t.Printer, t.Err
}, },
Validator: func() (validation.Schema, error) { Validator: func() (validation.Schema, error) {
...@@ -243,7 +243,7 @@ func stringBody(body string) io.ReadCloser { ...@@ -243,7 +243,7 @@ func stringBody(body string) io.ReadCloser {
func ExamplePrintReplicationController() { func ExamplePrintReplicationController() {
f, tf, codec := NewAPIFactory() f, tf, codec := NewAPIFactory()
tf.Printer = kubectl.NewHumanReadablePrinter(false, false) tf.Printer = kubectl.NewHumanReadablePrinter(false, false, []string{})
tf.Client = &client.FakeRESTClient{ tf.Client = &client.FakeRESTClient{
Codec: codec, Codec: codec,
Client: nil, Client: nil,
......
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
"github.com/spf13/cobra" "github.com/spf13/cobra"
...@@ -61,7 +62,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7` ...@@ -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 // NewCmdGet creates a command object for the generic "get" action, which
// retrieves one or more resources from a server. // retrieves one or more resources from a server.
func NewCmdGet(f *cmdutil.Factory, out io.Writer) *cobra.Command { func NewCmdGet(f *cmdutil.Factory, out io.Writer) *cobra.Command {
p := kubectl.NewHumanReadablePrinter(false, false) p := kubectl.NewHumanReadablePrinter(false, false, []string{})
validArgs := p.HandledResources() validArgs := p.HandledResources()
cmd := &cobra.Command{ cmd := &cobra.Command{
...@@ -80,6 +81,7 @@ func NewCmdGet(f *cmdutil.Factory, out io.Writer) *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().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("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.") 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 return cmd
} }
......
...@@ -64,7 +64,7 @@ type Factory struct { ...@@ -64,7 +64,7 @@ type Factory struct {
// Returns a Describer for displaying the specified RESTMapping type or an error. // Returns a Describer for displaying the specified RESTMapping type or an error.
Describer func(mapping *meta.RESTMapping) (kubectl.Describer, error) Describer func(mapping *meta.RESTMapping) (kubectl.Describer, error)
// Returns a Printer for formatting objects of the given type or an 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 // Returns a Scaler for changing the size of the specified RESTMapping type or an error
Scaler func(mapping *meta.RESTMapping) (kubectl.Scaler, error) Scaler func(mapping *meta.RESTMapping) (kubectl.Scaler, error)
// Returns a Reaper for gracefully shutting down resources. // Returns a Reaper for gracefully shutting down resources.
...@@ -140,8 +140,8 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { ...@@ -140,8 +140,8 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
} }
return describer, nil return describer, nil
}, },
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 kubectl.NewHumanReadablePrinter(noHeaders, withNamespace), nil return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace, columnLabels), nil
}, },
PodSelectorForObject: func(object runtime.Object) (string, error) { PodSelectorForObject: func(object runtime.Object) (string, error) {
// TODO: replace with a swagger schema based approach (identify pod selector via schema introspection) // 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 ...@@ -378,7 +378,7 @@ func (f *Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMappin
} }
printer = kubectl.NewVersionedPrinter(printer, mapping.ObjectConvertor, version, mapping.APIVersion) printer = kubectl.NewVersionedPrinter(printer, mapping.ObjectConvertor, version, mapping.APIVersion)
} else { } 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 { if err != nil {
return nil, err return nil, err
} }
......
...@@ -35,6 +35,7 @@ import ( ...@@ -35,6 +35,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
"github.com/evanphx/json-patch" "github.com/evanphx/json-patch"
...@@ -250,6 +251,15 @@ func GetFlagString(cmd *cobra.Command, flag string) string { ...@@ -250,6 +251,15 @@ func GetFlagString(cmd *cobra.Command, flag string) string {
return f.Value.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 { func GetFlagBool(cmd *cobra.Command, flag string) bool {
f := getFlag(cmd, flag) f := getFlag(cmd, flag)
result, err := strconv.ParseBool(f.Value.String()) result, err := strconv.ParseBool(f.Value.String())
......
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra" "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) { func AddPrinterFlags(cmd *cobra.Command) {
cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|template|templatefile.") 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).") cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).")
......
...@@ -237,18 +237,18 @@ type TestUnknownType struct{} ...@@ -237,18 +237,18 @@ type TestUnknownType struct{}
func (*TestUnknownType) IsAnAPIObject() {} 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) _, err := fmt.Fprintf(w, "%s", obj.Data)
return err 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") return fmt.Errorf("ErrorPrintHandler error")
} }
func TestCustomTypePrinting(t *testing.T) { func TestCustomTypePrinting(t *testing.T) {
columns := []string{"Data"} columns := []string{"Data"}
printer := NewHumanReadablePrinter(false, false) printer := NewHumanReadablePrinter(false, false, []string{})
printer.Handler(columns, PrintCustomType) printer.Handler(columns, PrintCustomType)
obj := TestPrintType{"test object"} obj := TestPrintType{"test object"}
...@@ -265,7 +265,7 @@ func TestCustomTypePrinting(t *testing.T) { ...@@ -265,7 +265,7 @@ func TestCustomTypePrinting(t *testing.T) {
func TestPrintHandlerError(t *testing.T) { func TestPrintHandlerError(t *testing.T) {
columns := []string{"Data"} columns := []string{"Data"}
printer := NewHumanReadablePrinter(false, false) printer := NewHumanReadablePrinter(false, false, []string{})
printer.Handler(columns, ErrorPrintHandler) printer.Handler(columns, ErrorPrintHandler)
obj := TestPrintType{"test object"} obj := TestPrintType{"test object"}
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
...@@ -276,7 +276,7 @@ func TestPrintHandlerError(t *testing.T) { ...@@ -276,7 +276,7 @@ func TestPrintHandlerError(t *testing.T) {
} }
func TestUnknownTypePrinting(t *testing.T) { func TestUnknownTypePrinting(t *testing.T) {
printer := NewHumanReadablePrinter(false, false) printer := NewHumanReadablePrinter(false, false, []string{})
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
err := printer.PrintObj(&TestUnknownType{}, buffer) err := printer.PrintObj(&TestUnknownType{}, buffer)
if err == nil { if err == nil {
...@@ -452,8 +452,8 @@ func TestPrinters(t *testing.T) { ...@@ -452,8 +452,8 @@ func TestPrinters(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
printers := map[string]ResourcePrinter{ printers := map[string]ResourcePrinter{
"humanReadable": NewHumanReadablePrinter(true, false), "humanReadable": NewHumanReadablePrinter(true, false, []string{}),
"humanReadableHeaders": NewHumanReadablePrinter(false, false), "humanReadableHeaders": NewHumanReadablePrinter(false, false, []string{}),
"json": &JSONPrinter{}, "json": &JSONPrinter{},
"yaml": &YAMLPrinter{}, "yaml": &YAMLPrinter{},
"template": templatePrinter, "template": templatePrinter,
...@@ -490,7 +490,7 @@ func TestPrinters(t *testing.T) { ...@@ -490,7 +490,7 @@ func TestPrinters(t *testing.T) {
func TestPrintEventsResultSorted(t *testing.T) { func TestPrintEventsResultSorted(t *testing.T) {
// Arrange // Arrange
printer := NewHumanReadablePrinter(false /* noHeaders */, false) printer := NewHumanReadablePrinter(false /* noHeaders */, false, []string{})
obj := api.EventList{ obj := api.EventList{
Items: []api.Event{ Items: []api.Event{
...@@ -531,7 +531,7 @@ func TestPrintEventsResultSorted(t *testing.T) { ...@@ -531,7 +531,7 @@ func TestPrintEventsResultSorted(t *testing.T) {
} }
func TestPrintMinionStatus(t *testing.T) { func TestPrintMinionStatus(t *testing.T) {
printer := NewHumanReadablePrinter(false, false) printer := NewHumanReadablePrinter(false, false, []string{})
table := []struct { table := []struct {
minion api.Node minion api.Node
status string status string
...@@ -739,7 +739,7 @@ func TestPrintHumanReadableService(t *testing.T) { ...@@ -739,7 +739,7 @@ func TestPrintHumanReadableService(t *testing.T) {
for _, svc := range tests { for _, svc := range tests {
buff := bytes.Buffer{} buff := bytes.Buffer{}
printService(&svc, &buff, false) printService(&svc, &buff, false, []string{})
output := string(buff.Bytes()) output := string(buff.Bytes())
ip := svc.Spec.ClusterIP ip := svc.Spec.ClusterIP
if !strings.Contains(output, ip) { if !strings.Contains(output, ip) {
...@@ -921,7 +921,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { ...@@ -921,7 +921,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) {
}, },
} }
printer := NewHumanReadablePrinter(false, false) printer := NewHumanReadablePrinter(false, false, []string{})
for _, test := range table { for _, test := range table {
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
err := printer.PrintObj(test.obj, buffer) err := printer.PrintObj(test.obj, buffer)
...@@ -934,7 +934,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { ...@@ -934,7 +934,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) {
} }
} }
printer = NewHumanReadablePrinter(false, true) printer = NewHumanReadablePrinter(false, true, []string{})
for _, test := range table { for _, test := range table {
buffer := &bytes.Buffer{} buffer := &bytes.Buffer{}
err := printer.PrintObj(test.obj, buffer) err := printer.PrintObj(test.obj, buffer)
...@@ -1019,7 +1019,7 @@ func TestPrintPod(t *testing.T) { ...@@ -1019,7 +1019,7 @@ func TestPrintPod(t *testing.T) {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
for _, test := range tests { for _, test := range tests {
printPod(&test.pod, buf, false) printPod(&test.pod, buf, false, []string{})
// We ignore time // We ignore time
if !strings.HasPrefix(buf.String(), test.expect) { if !strings.HasPrefix(buf.String(), test.expect) {
t.Fatalf("Expected: %s, got: %s", test.expect, buf.String()) t.Fatalf("Expected: %s, got: %s", test.expect, buf.String())
...@@ -1027,3 +1027,63 @@ func TestPrintPod(t *testing.T) { ...@@ -1027,3 +1027,63 @@ func TestPrintPod(t *testing.T) {
buf.Reset() 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()
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment