From 682734bcc776a473a31b14920cc55f34c671a91e Mon Sep 17 00:00:00 2001 From: Janet Kuo Date: Mon, 29 Jun 2015 11:36:06 -0700 Subject: [PATCH] Implement 'kubectl get ... -o wide' --- docs/kubectl_config_view.md | 4 +- docs/kubectl_expose.md | 4 +- docs/kubectl_get.md | 11 +- docs/kubectl_label.md | 4 +- docs/kubectl_rolling-update.md | 4 +- docs/kubectl_run.md | 4 +- docs/man/man1/kubectl-config-view.1 | 2 +- docs/man/man1/kubectl-expose.1 | 2 +- docs/man/man1/kubectl-get.1 | 7 +- docs/man/man1/kubectl-label.1 | 2 +- docs/man/man1/kubectl-rolling-update.1 | 2 +- docs/man/man1/kubectl-run.1 | 2 +- pkg/kubectl/cmd/cmd_test.go | 38 ++++++- pkg/kubectl/cmd/get.go | 9 +- pkg/kubectl/cmd/util/factory.go | 8 +- pkg/kubectl/cmd/util/helpers.go | 9 ++ pkg/kubectl/cmd/util/printing.go | 2 +- pkg/kubectl/resource_printer.go | 139 +++++++++++++++---------- pkg/kubectl/resource_printer_test.go | 28 ++--- 19 files changed, 179 insertions(+), 102 deletions(-) diff --git a/docs/kubectl_config_view.md b/docs/kubectl_config_view.md index 18a8f4f3e85..713ae2e54a4 100644 --- a/docs/kubectl_config_view.md +++ b/docs/kubectl_config_view.md @@ -31,7 +31,7 @@ $ kubectl config view -o template --template='{{range .users}}{{ if eq .name "e2 --merge=true: merge together the full hierarchy of kubeconfig files --minify=false: remove all information not used by current-context from the output --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|wide. --output-version="": Output the formatted object with the given version (default api-version). --raw=false: display raw byte data -t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview] @@ -69,6 +69,6 @@ $ kubectl config view -o template --template='{{range .users}}{{ if eq .name "e2 ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-06-09 19:55:35.92095292 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-30 16:51:25.60964036 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_config_view.md?pixel)]() diff --git a/docs/kubectl_expose.md b/docs/kubectl_expose.md index 8392dc762e6..ccdea8d157d 100644 --- a/docs/kubectl_expose.md +++ b/docs/kubectl_expose.md @@ -39,7 +39,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream -l, --labels="": Labels to apply to the service created by this call. --name="": The name for the newly created object. --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|wide. --output-version="": Output the formatted object with the given version (default api-version). --overrides="": An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. --port=-1: The port that the service should serve on. Required. @@ -83,6 +83,6 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-02 11:05:52.857144556 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-30 16:51:25.609406207 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_expose.md?pixel)]() diff --git a/docs/kubectl_get.md b/docs/kubectl_get.md index df7df373611..6d77854b0a1 100644 --- a/docs/kubectl_get.md +++ b/docs/kubectl_get.md @@ -16,7 +16,7 @@ By specifying the output as 'template' and providing a Go template as the value of the --template flag, you can filter the attributes of the fetched resource(s). ``` -kubectl get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...) +kubectl get [(-o|--output=)json|yaml|template|wide|...] (RESOURCE [NAME] | RESOURCE/NAME ...) ``` ### Examples @@ -25,6 +25,9 @@ kubectl get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/N // List all pods in ps output format. $ kubectl get pods +// List all pods in ps output format with more information (such as node name). +$ kubectl get pods -o wide + // List a single replication controller with specified NAME in ps output format. $ kubectl get replicationcontroller web @@ -37,7 +40,7 @@ $ kubectl get -o template web-pod-13je7 --template={{.status.phase}} --api-versi // List all replication controllers and services together in ps output format. $ kubectl get rc,services -// List one or more resources by their type and names +// List one or more resources by their type and names. $ kubectl get rc/web service/frontend pods/web-pod-13je7 ``` @@ -48,7 +51,7 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 -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. + -o, --output="": Output format. One of: json|yaml|template|templatefile|wide. --output-version="": Output the formatted object with the given version (default api-version). -l, --selector="": Selector (label query) to filter on -t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview] @@ -88,6 +91,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-21 22:41:03.746552518 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-30 18:05:17.889608636 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_get.md?pixel)]() diff --git a/docs/kubectl_label.md b/docs/kubectl_label.md index fa0bbaa83ec..4e30a126d2e 100644 --- a/docs/kubectl_label.md +++ b/docs/kubectl_label.md @@ -41,7 +41,7 @@ $ kubectl label pods foo bar- --all=false: select all resources in the namespace of the specified resource types -h, --help=false: help for label --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|wide. --output-version="": Output the formatted object with the given version (default api-version). --overwrite=false: If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels. --resource-version="": If non-empty, the labels update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource. @@ -81,6 +81,6 @@ $ kubectl label pods foo bar- ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-05-28 08:44:48.996047458 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-30 16:51:25.609525621 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_label.md?pixel)]() diff --git a/docs/kubectl_rolling-update.md b/docs/kubectl_rolling-update.md index 1a2b338bb28..f3d4c9d647f 100644 --- a/docs/kubectl_rolling-update.md +++ b/docs/kubectl_rolling-update.md @@ -42,7 +42,7 @@ $ kubectl rolling-update frontend --image=image:v2 -h, --help=false: help for rolling-update --image="": Image to use for upgrading the replication controller. Can not be used with --filename/-f --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|wide. --output-version="": Output the formatted object with the given version (default api-version). --poll-interval="3s": Time delay between polling for replication controller status after the update. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". --rollback=false: If true, this is a request to abort an existing rollout that is partially rolled out. It effectively reverses current and next and runs a rollout @@ -83,6 +83,6 @@ $ kubectl rolling-update frontend --image=image:v2 ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-17 14:57:27.791796674 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-30 16:51:25.608180277 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_rolling-update.md?pixel)]() diff --git a/docs/kubectl_run.md b/docs/kubectl_run.md index f9469d4a864..324c390513b 100644 --- a/docs/kubectl_run.md +++ b/docs/kubectl_run.md @@ -38,7 +38,7 @@ $ kubectl run nginx --image=nginx --overrides='{ "apiVersion": "v1", "spec": { . --image="": The image for the container to run. -l, --labels="": Labels to apply to the pod(s). --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|wide. --output-version="": Output the formatted object with the given version (default api-version). --overrides="": An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. --port=-1: The port that this container exposes. @@ -78,6 +78,6 @@ $ kubectl run nginx --image=nginx --overrides='{ "apiVersion": "v1", "spec": { . ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-05 21:08:36.513272503 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-30 16:51:25.609119035 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_run.md?pixel)]() diff --git a/docs/man/man1/kubectl-config-view.1 b/docs/man/man1/kubectl-config-view.1 index c4e237d5818..7d3f21e43ab 100644 --- a/docs/man/man1/kubectl-config-view.1 +++ b/docs/man/man1/kubectl-config-view.1 @@ -42,7 +42,7 @@ You can use \-\-output=template \-\-template=TEMPLATE to extract specific values .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: json|yaml|template|templatefile. + Output format. One of: json|yaml|template|templatefile|wide. .PP \fB\-\-output\-version\fP="" diff --git a/docs/man/man1/kubectl-expose.1 b/docs/man/man1/kubectl-expose.1 index 55aaec9d511..dd2a2cc773f 100644 --- a/docs/man/man1/kubectl-expose.1 +++ b/docs/man/man1/kubectl-expose.1 @@ -56,7 +56,7 @@ re\-use the labels from the resource it exposes. .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: json|yaml|template|templatefile. + Output format. One of: json|yaml|template|templatefile|wide. .PP \fB\-\-output\-version\fP="" diff --git a/docs/man/man1/kubectl-get.1 b/docs/man/man1/kubectl-get.1 index dd250f11e12..e2f48814cf0 100644 --- a/docs/man/man1/kubectl-get.1 +++ b/docs/man/man1/kubectl-get.1 @@ -45,7 +45,7 @@ of the \-\-template flag, you can filter the attributes of the fetched resource( .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: json|yaml|template|templatefile. + Output format. One of: json|yaml|template|templatefile|wide. .PP \fB\-\-output\-version\fP="" @@ -175,6 +175,9 @@ of the \-\-template flag, you can filter the attributes of the fetched resource( // List all pods in ps output format. $ kubectl get pods +// List all pods in ps output format with more information (such as node name). +$ kubectl get pods \-o wide + // List a single replication controller with specified NAME in ps output format. $ kubectl get replicationcontroller web @@ -187,7 +190,7 @@ $ kubectl get \-o template web\-pod\-13je7 \-\-template=\{\{.status.phase\}\} \- // List all replication controllers and services together in ps output format. $ kubectl get rc,services -// List one or more resources by their type and names +// List one or more resources by their type and names. $ kubectl get rc/web service/frontend pods/web\-pod\-13je7 .fi diff --git a/docs/man/man1/kubectl-label.1 b/docs/man/man1/kubectl-label.1 index 9fddbcd4a07..a74ab28140a 100644 --- a/docs/man/man1/kubectl-label.1 +++ b/docs/man/man1/kubectl-label.1 @@ -36,7 +36,7 @@ If \-\-resource\-version is specified, then updates will use this resource versi .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: json|yaml|template|templatefile. + Output format. One of: json|yaml|template|templatefile|wide. .PP \fB\-\-output\-version\fP="" diff --git a/docs/man/man1/kubectl-rolling-update.1 b/docs/man/man1/kubectl-rolling-update.1 index b1eae5e2c38..caf05d027c8 100644 --- a/docs/man/man1/kubectl-rolling-update.1 +++ b/docs/man/man1/kubectl-rolling-update.1 @@ -48,7 +48,7 @@ existing replication controller and overwrite at least one (common) label in its .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: json|yaml|template|templatefile. + Output format. One of: json|yaml|template|templatefile|wide. .PP \fB\-\-output\-version\fP="" diff --git a/docs/man/man1/kubectl-run.1 b/docs/man/man1/kubectl-run.1 index 10244c69d65..d193eacf20b 100644 --- a/docs/man/man1/kubectl-run.1 +++ b/docs/man/man1/kubectl-run.1 @@ -48,7 +48,7 @@ Creates a replication controller to manage the created container(s). .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: json|yaml|template|templatefile. + Output format. One of: json|yaml|template|templatefile|wide. .PP \fB\-\-output\-version\fP="" diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 7991a57b776..7270581ace4 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, columnLabels []string) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide 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, columnLabels []string) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide 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, []string{}) + tf.Printer = kubectl.NewHumanReadablePrinter(false, false, false, []string{}) tf.Client = &client.FakeRESTClient{ Codec: codec, Client: nil, @@ -281,6 +281,38 @@ func ExamplePrintReplicationController() { // foo foo someimage foo=bar 1 } +func ExamplePrintPodWithWideFormat() { + f, tf, codec := NewAPIFactory() + tf.Printer = kubectl.NewHumanReadablePrinter(false, false, true, []string{}) + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: nil, + } + nodeName := "kubernetes-minion-abcd" + cmd := NewCmdRun(f, os.Stdout) + ctrl := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "test1"}, + Spec: api.PodSpec{ + Containers: make([]api.Container, 2), + NodeName: nodeName, + }, + Status: api.PodStatus{ + Phase: "podPhase", + ContainerStatuses: []api.ContainerStatus{ + {Ready: true, RestartCount: 3, State: api.ContainerState{Running: &api.ContainerStateRunning{}}}, + {RestartCount: 3}, + }, + }, + } + err := f.PrintObject(cmd, ctrl, os.Stdout) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + // Output: + // NAME READY REASON RESTARTS AGE NODE + // test1 1/2 podPhase 6 292y kubernetes-minion-abcd +} + func TestNormalizationFuncGlobalExistance(t *testing.T) { // This test can be safely deleted when we will not support multiple flag formats root := NewKubectlCommand(cmdutil.NewFactory(nil), os.Stdin, os.Stdout, os.Stderr) diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 8cd8f506aaa..5d5afcab209 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -43,6 +43,9 @@ of the --template flag, you can filter the attributes of the fetched resource(s) get_example = `// List all pods in ps output format. $ kubectl get pods +// List all pods in ps output format with more information (such as node name). +$ kubectl get pods -o wide + // List a single replication controller with specified NAME in ps output format. $ kubectl get replicationcontroller web @@ -55,18 +58,18 @@ $ kubectl get -o template web-pod-13je7 --template={{.status.phase}} --api-versi // List all replication controllers and services together in ps output format. $ kubectl get rc,services -// List one or more resources by their type and names +// List one or more resources by their type and names. $ 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, []string{}) + p := kubectl.NewHumanReadablePrinter(false, false, false, []string{}) validArgs := p.HandledResources() cmd := &cobra.Command{ - Use: "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)", + Use: "get [(-o|--output=)json|yaml|template|wide|...] (RESOURCE [NAME] | RESOURCE/NAME ...)", Short: "Display one or many resources", Long: get_long, Example: get_example, diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 34493380c3b..c24a0eee3ca 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -65,7 +65,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, columnLabels []string) (kubectl.ResourcePrinter, error) + Printer func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide 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. @@ -141,8 +141,8 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return describer, nil }, - Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, columnLabels []string) (kubectl.ResourcePrinter, error) { - return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace, columnLabels), nil + Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide bool, columnLabels []string) (kubectl.ResourcePrinter, error) { + return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace, wide, columnLabels), nil }, PodSelectorForObject: func(object runtime.Object) (string, error) { // TODO: replace with a swagger schema based approach (identify pod selector via schema introspection) @@ -382,7 +382,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, GetFlagStringList(cmd, "label-columns")) + printer, err = f.Printer(mapping, GetFlagBool(cmd, "no-headers"), withNamespace, GetWideFlag(cmd), 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 7bde9c1b11e..c91138525bb 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -275,6 +275,15 @@ func GetFlagStringList(cmd *cobra.Command, flag string) util.StringList { return *f.Value.(*util.StringList) } +// GetWideFlag is used to determine if "-o wide" is used +func GetWideFlag(cmd *cobra.Command) bool { + f := cmd.Flags().Lookup("output") + if f.Value.String() == "wide" { + return true + } + return false +} + 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 ab646fbe616..966cbb3baa2 100644 --- a/pkg/kubectl/cmd/util/printing.go +++ b/pkg/kubectl/cmd/util/printing.go @@ -24,7 +24,7 @@ import ( // 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().StringP("output", "o", "", "Output format. One of: json|yaml|template|templatefile|wide.") cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).") cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers.") cmd.Flags().StringP("template", "t", "", "Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]") diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 606e17979c9..eb116b8aa07 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -72,6 +72,8 @@ func GetPrinter(format, formatArgument string) (ResourcePrinter, bool, error) { if err != nil { return nil, false, fmt.Errorf("error parsing template %s, %v\n", string(data), err) } + case "wide": + fallthrough case "": return nil, false, nil default: @@ -180,16 +182,18 @@ type HumanReadablePrinter struct { handlerMap map[reflect.Type]*handlerEntry noHeaders bool withNamespace bool + wide bool columnLabels []string lastType reflect.Type } // NewHumanReadablePrinter creates a HumanReadablePrinter. -func NewHumanReadablePrinter(noHeaders, withNamespace bool, columnLabels []string) *HumanReadablePrinter { +func NewHumanReadablePrinter(noHeaders, withNamespace bool, wide bool, columnLabels []string) *HumanReadablePrinter { printer := &HumanReadablePrinter{ handlerMap: make(map[reflect.Type]*handlerEntry), noHeaders: noHeaders, withNamespace: withNamespace, + wide: wide, columnLabels: columnLabels, } printer.addDefaultHandlers() @@ -197,10 +201,7 @@ func NewHumanReadablePrinter(noHeaders, withNamespace bool, columnLabels []strin } // Handler adds a print handler with a given set of columns to HumanReadablePrinter instance. -// printFunc is the function that will be called to print an object. -// It must be of the following type: -// func printFunc(object ObjectType, w io.Writer, withNamespace bool) error -// where ObjectType is the type of the object that will be printed. +// See validatePrintHandlerFunc for required method signature. func (h *HumanReadablePrinter) Handler(columns []string, printFunc interface{}) error { printFuncValue := reflect.ValueOf(printFunc) if err := h.validatePrintHandlerFunc(printFuncValue); err != nil { @@ -215,20 +216,25 @@ func (h *HumanReadablePrinter) Handler(columns []string, printFunc interface{}) return nil } +// validatePrintHandlerFunc validates print handler signature. +// printFunc is the function that will be called to print an object. +// It must be of the following type: +// func printFunc(object ObjectType, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error +// where ObjectType is the type of the object that will be printed. 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) } funcType := printFunc.Type() - if funcType.NumIn() != 4 || funcType.NumOut() != 1 { + if funcType.NumIn() != 5 || funcType.NumOut() != 1 { return fmt.Errorf("invalid print handler." + - "Must accept 3 parameters and return 1 value.") + "Must accept 5 parameters and return 1 value.") } if funcType.In(1) != reflect.TypeOf((*io.Writer)(nil)).Elem() || - funcType.In(3) != reflect.TypeOf((*[]string)(nil)).Elem() || + funcType.In(4) != 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, columnLabels []string) error", funcType.In(0)) + "func handler(obj %v, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error", funcType.In(0)) } return nil } @@ -371,7 +377,7 @@ func translateTimestamp(timestamp util.Time) string { return shortHumanDuration(time.Now().Sub(timestamp.Time)) } -func printPod(pod *api.Pod, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPod(pod *api.Pod, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { name := pod.Name if withNamespace { name = types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}.String() @@ -415,20 +421,30 @@ func printPod(pod *api.Pod, w io.Writer, withNamespace bool, columnLabels []stri ); err != nil { return err } + + if wide { + nodeName := pod.Spec.NodeName + if _, err := fmt.Fprintf(w, "\t%s", + nodeName, + ); err != nil { + return err + } + } + _, err := fmt.Fprint(w, appendLabels(pod.Labels, columnLabels)) return err } -func printPodList(podList *api.PodList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPodList(podList *api.PodList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, pod := range podList.Items { - if err := printPod(&pod, w, withNamespace, columnLabels); err != nil { + if err := printPod(&pod, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{pod.Namespace, pod.Name}.String() @@ -464,16 +480,16 @@ func printPodTemplate(pod *api.PodTemplate, w io.Writer, withNamespace bool, col return nil } -func printPodTemplateList(podList *api.PodTemplateList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPodTemplateList(podList *api.PodTemplateList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, pod := range podList.Items { - if err := printPodTemplate(&pod, w, withNamespace, columnLabels); err != nil { + if err := printPodTemplate(&pod, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printReplicationController(controller *api.ReplicationController, w io.Writer, withNamespace bool, columnLabels []string) error { +func printReplicationController(controller *api.ReplicationController, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{controller.Namespace, controller.Name}.String() @@ -510,16 +526,16 @@ func printReplicationController(controller *api.ReplicationController, w io.Writ return nil } -func printReplicationControllerList(list *api.ReplicationControllerList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printReplicationControllerList(list *api.ReplicationControllerList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, controller := range list.Items { - if err := printReplicationController(&controller, w, withNamespace, columnLabels); err != nil { + if err := printReplicationController(&controller, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printService(svc *api.Service, w io.Writer, withNamespace bool, columnLabels []string) error { +func printService(svc *api.Service, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{svc.Namespace, svc.Name}.String() @@ -566,16 +582,16 @@ func printService(svc *api.Service, w io.Writer, withNamespace bool, columnLabel return nil } -func printServiceList(list *api.ServiceList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printServiceList(list *api.ServiceList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, svc := range list.Items { - if err := printService(&svc, w, withNamespace, columnLabels); err != nil { + if err := printService(&svc, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printEndpoints(endpoints *api.Endpoints, w io.Writer, withNamespace bool, columnLabels []string) error { +func printEndpoints(endpoints *api.Endpoints, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{endpoints.Namespace, endpoints.Name}.String() @@ -590,16 +606,16 @@ func printEndpoints(endpoints *api.Endpoints, w io.Writer, withNamespace bool, c return err } -func printEndpointsList(list *api.EndpointsList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printEndpointsList(list *api.EndpointsList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, item := range list.Items { - if err := printEndpoints(&item, w, withNamespace, columnLabels); err != nil { + if err := printEndpoints(&item, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printNamespace(item *api.Namespace, w io.Writer, withNamespace bool, columnLabels []string) error { +func printNamespace(item *api.Namespace, w io.Writer, withNamespace bool, wide 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 } @@ -607,16 +623,16 @@ func printNamespace(item *api.Namespace, w io.Writer, withNamespace bool, column return err } -func printNamespaceList(list *api.NamespaceList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printNamespaceList(list *api.NamespaceList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, item := range list.Items { - if err := printNamespace(&item, w, withNamespace, columnLabels); err != nil { + if err := printNamespace(&item, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printSecret(item *api.Secret, w io.Writer, withNamespace bool, columnLabels []string) error { +func printSecret(item *api.Secret, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{item.Namespace, item.Name}.String() @@ -631,9 +647,9 @@ func printSecret(item *api.Secret, w io.Writer, withNamespace bool, columnLabels return err } -func printSecretList(list *api.SecretList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printSecretList(list *api.SecretList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, item := range list.Items { - if err := printSecret(&item, w, withNamespace, columnLabels); err != nil { + if err := printSecret(&item, w, withNamespace, wide, columnLabels); err != nil { return err } } @@ -641,7 +657,7 @@ func printSecretList(list *api.SecretList, w io.Writer, withNamespace bool, colu return nil } -func printServiceAccount(item *api.ServiceAccount, w io.Writer, withNamespace bool, columnLabels []string) error { +func printServiceAccount(item *api.ServiceAccount, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{item.Namespace, item.Name}.String() @@ -656,9 +672,9 @@ func printServiceAccount(item *api.ServiceAccount, w io.Writer, withNamespace bo return err } -func printServiceAccountList(list *api.ServiceAccountList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printServiceAccountList(list *api.ServiceAccountList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, item := range list.Items { - if err := printServiceAccount(&item, w, withNamespace, columnLabels); err != nil { + if err := printServiceAccount(&item, w, withNamespace, wide, columnLabels); err != nil { return err } } @@ -666,7 +682,7 @@ func printServiceAccountList(list *api.ServiceAccountList, w io.Writer, withName return nil } -func printNode(node *api.Node, w io.Writer, withNamespace bool, columnLabels []string) error { +func printNode(node *api.Node, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { conditionMap := make(map[api.NodeConditionType]*api.NodeCondition) NodeAllConditions := []api.NodeConditionType{api.NodeReady} for i := range node.Status.Conditions { @@ -697,16 +713,16 @@ func printNode(node *api.Node, w io.Writer, withNamespace bool, columnLabels []s return err } -func printNodeList(list *api.NodeList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printNodeList(list *api.NodeList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, node := range list.Items { - if err := printNode(&node, w, withNamespace, columnLabels); err != nil { + if err := printNode(&node, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printPersistentVolume(pv *api.PersistentVolume, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPersistentVolume(pv *api.PersistentVolume, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{pv.Namespace, pv.Name}.String() @@ -733,16 +749,16 @@ func printPersistentVolume(pv *api.PersistentVolume, w io.Writer, withNamespace return err } -func printPersistentVolumeList(list *api.PersistentVolumeList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPersistentVolumeList(list *api.PersistentVolumeList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, pv := range list.Items { - if err := printPersistentVolume(&pv, w, withNamespace, columnLabels); err != nil { + if err := printPersistentVolume(&pv, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{pvc.Namespace, pvc.Name}.String() @@ -757,16 +773,16 @@ func printPersistentVolumeClaim(pvc *api.PersistentVolumeClaim, w io.Writer, wit return err } -func printPersistentVolumeClaimList(list *api.PersistentVolumeClaimList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printPersistentVolumeClaimList(list *api.PersistentVolumeClaimList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, psd := range list.Items { - if err := printPersistentVolumeClaim(&psd, w, withNamespace, columnLabels); err != nil { + if err := printPersistentVolumeClaim(&psd, w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printEvent(event *api.Event, w io.Writer, withNamespace bool, columnLabels []string) error { +func printEvent(event *api.Event, w io.Writer, withNamespace bool, wide 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), @@ -786,17 +802,17 @@ func printEvent(event *api.Event, w io.Writer, withNamespace bool, columnLabels } // Sorts and prints the EventList in a human-friendly format. -func printEventList(list *api.EventList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printEventList(list *api.EventList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { sort.Sort(SortableEvents(list.Items)) for i := range list.Items { - if err := printEvent(&list.Items[i], w, withNamespace, columnLabels); err != nil { + if err := printEvent(&list.Items[i], w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printLimitRange(limitRange *api.LimitRange, w io.Writer, withNamespace bool, columnLabels []string) error { +func printLimitRange(limitRange *api.LimitRange, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{limitRange.Namespace, limitRange.Name}.String() @@ -812,16 +828,16 @@ func printLimitRange(limitRange *api.LimitRange, w io.Writer, withNamespace bool } // Prints the LimitRangeList in a human-friendly format. -func printLimitRangeList(list *api.LimitRangeList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printLimitRangeList(list *api.LimitRangeList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for i := range list.Items { - if err := printLimitRange(&list.Items[i], w, withNamespace, columnLabels); err != nil { + if err := printLimitRange(&list.Items[i], w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, withNamespace bool, columnLabels []string) error { +func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { var name string if withNamespace { name = types.NamespacedName{resourceQuota.Namespace, resourceQuota.Name}.String() @@ -837,16 +853,16 @@ func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, withNames } // Prints the ResourceQuotaList in a human-friendly format. -func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for i := range list.Items { - if err := printResourceQuota(&list.Items[i], w, withNamespace, columnLabels); err != nil { + if err := printResourceQuota(&list.Items[i], w, withNamespace, wide, columnLabels); err != nil { return err } } return nil } -func printComponentStatus(item *api.ComponentStatus, w io.Writer, withNamespace bool, columnLabels []string) error { +func printComponentStatus(item *api.ComponentStatus, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { status := "Unknown" message := "" error := "" @@ -870,9 +886,9 @@ func printComponentStatus(item *api.ComponentStatus, w io.Writer, withNamespace return err } -func printComponentStatusList(list *api.ComponentStatusList, w io.Writer, withNamespace bool, columnLabels []string) error { +func printComponentStatusList(list *api.ComponentStatusList, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { for _, item := range list.Items { - if err := printComponentStatus(&item, w, withNamespace, columnLabels); err != nil { + if err := printComponentStatus(&item, w, withNamespace, wide, columnLabels); err != nil { return err } } @@ -905,6 +921,16 @@ func formatLabelHeaders(columnLabels []string) []string { return formHead } +// headers for -o wide +func formatWideHeaders(wide bool, t reflect.Type) []string { + if wide { + if t.String() == "*api.Pod" || t.String() == "*api.PodList" { + return []string{"NODE"} + } + } + return nil +} + // 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) @@ -912,11 +938,12 @@ 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 { - headers := append(handler.columns, formatLabelHeaders(h.columnLabels)...) + headers := append(handler.columns, formatWideHeaders(h.wide, t)...) + headers = append(headers, formatLabelHeaders(h.columnLabels)...) h.printHeader(headers, w) h.lastType = t } - args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w), reflect.ValueOf(h.withNamespace), reflect.ValueOf(h.columnLabels)} + args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w), reflect.ValueOf(h.withNamespace), reflect.ValueOf(h.wide), 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 f578709f203..931ac560601 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, columnLabels []string) error { +func PrintCustomType(obj *TestPrintType, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { _, err := fmt.Fprintf(w, "%s", obj.Data) return err } -func ErrorPrintHandler(obj *TestPrintType, w io.Writer, withNamespace bool, columnLabels []string) error { +func ErrorPrintHandler(obj *TestPrintType, w io.Writer, withNamespace bool, wide bool, columnLabels []string) error { return fmt.Errorf("ErrorPrintHandler error") } func TestCustomTypePrinting(t *testing.T) { columns := []string{"Data"} - printer := NewHumanReadablePrinter(false, false, []string{}) + printer := NewHumanReadablePrinter(false, 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, []string{}) + printer := NewHumanReadablePrinter(false, 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, []string{}) + printer := NewHumanReadablePrinter(false, 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, []string{}), - "humanReadableHeaders": NewHumanReadablePrinter(false, false, []string{}), + "humanReadable": NewHumanReadablePrinter(true, false, false, []string{}), + "humanReadableHeaders": NewHumanReadablePrinter(false, 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, []string{}) + printer := NewHumanReadablePrinter(false /* noHeaders */, false, 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, []string{}) + printer := NewHumanReadablePrinter(false, 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, []string{}) + printService(&svc, &buff, false, 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, []string{}) + printer := NewHumanReadablePrinter(false, 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, []string{}) + printer = NewHumanReadablePrinter(false, true, false, []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, []string{}) + printPod(&test.pod, buf, false, false, []string{}) // We ignore time if !strings.HasPrefix(buf.String(), test.expect) { t.Fatalf("Expected: %s, got: %s", test.expect, buf.String()) @@ -1095,7 +1095,7 @@ func TestPrintPodWithLabels(t *testing.T) { buf := bytes.NewBuffer([]byte{}) for _, test := range tests { - printPod(&test.pod, buf, false, test.labelColumns) + printPod(&test.pod, buf, false, 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())