mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
Add ability to output watch events from kubectl get
This commit is contained in:
parent
6e8012bac9
commit
13273468b4
@ -28,6 +28,8 @@ import (
|
|||||||
"github.com/liggitt/tabwriter"
|
"github.com/liggitt/tabwriter"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/cli-runtime/pkg/printers"
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
"k8s.io/client-go/util/jsonpath"
|
"k8s.io/client-go/util/jsonpath"
|
||||||
@ -205,6 +207,21 @@ func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error
|
|||||||
func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jsonpath.JSONPath, out io.Writer) error {
|
func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jsonpath.JSONPath, out io.Writer) error {
|
||||||
columns := make([]string, len(parsers))
|
columns := make([]string, len(parsers))
|
||||||
switch u := obj.(type) {
|
switch u := obj.(type) {
|
||||||
|
case *metav1.WatchEvent:
|
||||||
|
if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(u.Object.Object)).Type().PkgPath()) {
|
||||||
|
return fmt.Errorf(printers.InternalObjectPrinterErr)
|
||||||
|
}
|
||||||
|
unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(u.Object.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj = &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"type": u.Type,
|
||||||
|
"object": unstructuredObject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
case *runtime.Unknown:
|
case *runtime.Unknown:
|
||||||
if len(u.Raw) > 0 {
|
if len(u.Raw) > 0 {
|
||||||
var err error
|
var err error
|
||||||
|
@ -65,6 +65,8 @@ type GetOptions struct {
|
|||||||
WatchOnly bool
|
WatchOnly bool
|
||||||
ChunkSize int64
|
ChunkSize int64
|
||||||
|
|
||||||
|
OutputWatchEvents bool
|
||||||
|
|
||||||
LabelSelector string
|
LabelSelector string
|
||||||
FieldSelector string
|
FieldSelector string
|
||||||
AllNamespaces bool
|
AllNamespaces bool
|
||||||
@ -171,6 +173,7 @@ func NewCmdGet(parent string, f cmdutil.Factory, streams genericclioptions.IOStr
|
|||||||
cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to request from the server. Uses the transport specified by the kubeconfig file.")
|
cmd.Flags().StringVar(&o.Raw, "raw", o.Raw, "Raw URI to request from the server. Uses the transport specified by the kubeconfig file.")
|
||||||
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing/getting the requested object, watch for changes. Uninitialized objects are excluded if no object name is provided.")
|
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing/getting the requested object, watch for changes. Uninitialized objects are excluded if no object name is provided.")
|
||||||
cmd.Flags().BoolVar(&o.WatchOnly, "watch-only", o.WatchOnly, "Watch for changes to the requested object(s), without listing/getting first.")
|
cmd.Flags().BoolVar(&o.WatchOnly, "watch-only", o.WatchOnly, "Watch for changes to the requested object(s), without listing/getting first.")
|
||||||
|
cmd.Flags().BoolVar(&o.OutputWatchEvents, "output-watch-events", o.OutputWatchEvents, "Output watch event objects when --watch or --watch-only is used. Existing objects are output as initial ADDED events.")
|
||||||
cmd.Flags().Int64Var(&o.ChunkSize, "chunk-size", o.ChunkSize, "Return large lists in chunks rather than all at once. Pass 0 to disable. This flag is beta and may change in the future.")
|
cmd.Flags().Int64Var(&o.ChunkSize, "chunk-size", o.ChunkSize, "Return large lists in chunks rather than all at once. Pass 0 to disable. This flag is beta and may change in the future.")
|
||||||
cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.")
|
cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.")
|
||||||
cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||||
@ -309,6 +312,9 @@ func (o *GetOptions) Validate(cmd *cobra.Command) error {
|
|||||||
return fmt.Errorf("--show-labels option cannot be used with %s printer", outputOption)
|
return fmt.Errorf("--show-labels option cannot be used with %s printer", outputOption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if o.OutputWatchEvents && !(o.Watch || o.WatchOnly) {
|
||||||
|
return cmdutil.UsageErrorf(cmd, "--output-watch-events option can only be used with --watch or --watch-only")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,6 +670,9 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string)
|
|||||||
objsToPrint = append(objsToPrint, obj)
|
objsToPrint = append(objsToPrint, obj)
|
||||||
}
|
}
|
||||||
for _, objToPrint := range objsToPrint {
|
for _, objToPrint := range objsToPrint {
|
||||||
|
if o.OutputWatchEvents {
|
||||||
|
objToPrint = &metav1.WatchEvent{Type: string(watch.Added), Object: runtime.RawExtension{Object: objToPrint}}
|
||||||
|
}
|
||||||
if err := printer.PrintObj(objToPrint, writer); err != nil {
|
if err := printer.PrintObj(objToPrint, writer); err != nil {
|
||||||
return fmt.Errorf("unable to output the provided object: %v", err)
|
return fmt.Errorf("unable to output the provided object: %v", err)
|
||||||
}
|
}
|
||||||
@ -688,7 +697,11 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string)
|
|||||||
intr := interrupt.New(nil, cancel)
|
intr := interrupt.New(nil, cancel)
|
||||||
intr.Run(func() error {
|
intr.Run(func() error {
|
||||||
_, err := watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) {
|
_, err := watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) {
|
||||||
if err := printer.PrintObj(e.Object, writer); err != nil {
|
objToPrint := e.Object
|
||||||
|
if o.OutputWatchEvents {
|
||||||
|
objToPrint = &metav1.WatchEvent{Type: string(e.Type), Object: runtime.RawExtension{Object: objToPrint}}
|
||||||
|
}
|
||||||
|
if err := printer.PrintObj(objToPrint, writer); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
writer.Flush()
|
writer.Flush()
|
||||||
|
@ -2052,6 +2052,220 @@ b false
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWatchResourceWatchEvents(t *testing.T) {
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
format string
|
||||||
|
table bool
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
format: "",
|
||||||
|
expected: `EVENT NAMESPACE NAME AGE
|
||||||
|
ADDED test pod/bar <unknown>
|
||||||
|
ADDED test pod/foo <unknown>
|
||||||
|
MODIFIED test pod/foo <unknown>
|
||||||
|
DELETED test pod/foo <unknown>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "",
|
||||||
|
table: true,
|
||||||
|
expected: `EVENT NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||||
|
ADDED test pod/bar 0/0 0 <unknown>
|
||||||
|
ADDED test pod/foo 0/0 0 <unknown>
|
||||||
|
MODIFIED test pod/foo 0/0 0 <unknown>
|
||||||
|
DELETED test pod/foo 0/0 0 <unknown>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "wide",
|
||||||
|
table: true,
|
||||||
|
expected: `EVENT NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||||
|
ADDED test pod/bar 0/0 0 <unknown> <none> <none> <none> <none>
|
||||||
|
ADDED test pod/foo 0/0 0 <unknown> <none> <none> <none> <none>
|
||||||
|
MODIFIED test pod/foo 0/0 0 <unknown> <none> <none> <none> <none>
|
||||||
|
DELETED test pod/foo 0/0 0 <unknown> <none> <none> <none> <none>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "json",
|
||||||
|
expected: `{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"bar","namespace":"test","resourceVersion":"9"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||||
|
{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"10"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||||
|
{"type":"MODIFIED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"11"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||||
|
{"type":"DELETED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"creationTimestamp":null,"name":"foo","namespace":"test","resourceVersion":"12"},"spec":{"containers":null,"dnsPolicy":"ClusterFirst","enableServiceLinks":true,"restartPolicy":"Always","securityContext":{},"terminationGracePeriodSeconds":30},"status":{}}}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "yaml",
|
||||||
|
expected: `object:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: bar
|
||||||
|
namespace: test
|
||||||
|
resourceVersion: "9"
|
||||||
|
spec:
|
||||||
|
containers: null
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
enableServiceLinks: true
|
||||||
|
restartPolicy: Always
|
||||||
|
securityContext: {}
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
status: {}
|
||||||
|
type: ADDED
|
||||||
|
---
|
||||||
|
object:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: foo
|
||||||
|
namespace: test
|
||||||
|
resourceVersion: "10"
|
||||||
|
spec:
|
||||||
|
containers: null
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
enableServiceLinks: true
|
||||||
|
restartPolicy: Always
|
||||||
|
securityContext: {}
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
status: {}
|
||||||
|
type: ADDED
|
||||||
|
---
|
||||||
|
object:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: foo
|
||||||
|
namespace: test
|
||||||
|
resourceVersion: "11"
|
||||||
|
spec:
|
||||||
|
containers: null
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
enableServiceLinks: true
|
||||||
|
restartPolicy: Always
|
||||||
|
securityContext: {}
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
status: {}
|
||||||
|
type: MODIFIED
|
||||||
|
---
|
||||||
|
object:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
name: foo
|
||||||
|
namespace: test
|
||||||
|
resourceVersion: "12"
|
||||||
|
spec:
|
||||||
|
containers: null
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
enableServiceLinks: true
|
||||||
|
restartPolicy: Always
|
||||||
|
securityContext: {}
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
status: {}
|
||||||
|
type: DELETED
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: `jsonpath={.type},{.object.metadata.name},{.object.metadata.resourceVersion}{"\n"}`,
|
||||||
|
expected: `ADDED,bar,9
|
||||||
|
ADDED,foo,10
|
||||||
|
MODIFIED,foo,11
|
||||||
|
DELETED,foo,12
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: `go-template={{.type}},{{.object.metadata.name}},{{.object.metadata.resourceVersion}}{{"\n"}}`,
|
||||||
|
expected: `ADDED,bar,9
|
||||||
|
ADDED,foo,10
|
||||||
|
MODIFIED,foo,11
|
||||||
|
DELETED,foo,12
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: `custom-columns=TYPE:.type,NAME:.object.metadata.name,RSRC:.object.metadata.resourceVersion`,
|
||||||
|
expected: `TYPE NAME RSRC
|
||||||
|
ADDED bar 9
|
||||||
|
ADDED foo 10
|
||||||
|
MODIFIED foo 11
|
||||||
|
DELETED foo 12
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: `name`,
|
||||||
|
expected: `pod/bar
|
||||||
|
pod/foo
|
||||||
|
pod/foo
|
||||||
|
pod/foo
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(fmt.Sprintf("%s, table=%v", tc.format, tc.table), func(t *testing.T) {
|
||||||
|
pods, events := watchTestData()
|
||||||
|
|
||||||
|
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||||
|
defer tf.Cleanup()
|
||||||
|
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||||
|
|
||||||
|
podList := &corev1.PodList{
|
||||||
|
Items: pods,
|
||||||
|
ListMeta: metav1.ListMeta{
|
||||||
|
ResourceVersion: "10",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
|
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
|
||||||
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
|
switch req.URL.Path {
|
||||||
|
case "/pods":
|
||||||
|
if req.URL.Query().Get("watch") == "true" {
|
||||||
|
if tc.table {
|
||||||
|
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil
|
||||||
|
} else {
|
||||||
|
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events[2:])}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.table {
|
||||||
|
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil
|
||||||
|
} else {
|
||||||
|
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, podList)}, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("request url: %#v,and request: %#v", req.URL, req)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||||
|
cmd := NewCmdGet("kubectl", tf, streams)
|
||||||
|
cmd.SetOutput(buf)
|
||||||
|
|
||||||
|
cmd.Flags().Set("watch", "true")
|
||||||
|
cmd.Flags().Set("all-namespaces", "true")
|
||||||
|
cmd.Flags().Set("show-kind", "true")
|
||||||
|
cmd.Flags().Set("output-watch-events", "true")
|
||||||
|
if len(tc.format) > 0 {
|
||||||
|
cmd.Flags().Set("output", tc.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Run(cmd, []string{"pods"})
|
||||||
|
if e, a := tc.expected, buf.String(); e != a {
|
||||||
|
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWatchResourceIdentifiedByFile(t *testing.T) {
|
func TestWatchResourceIdentifiedByFile(t *testing.T) {
|
||||||
pods, events := watchTestData()
|
pods, events := watchTestData()
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -45,6 +46,11 @@ func (t *TablePrinter) PrintObj(obj runtime.Object, writer io.Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeIntoTable(obj runtime.Object) (runtime.Object, error) {
|
func decodeIntoTable(obj runtime.Object) (runtime.Object, error) {
|
||||||
|
event, isEvent := obj.(*metav1.WatchEvent)
|
||||||
|
if isEvent {
|
||||||
|
obj = event.Object.Object
|
||||||
|
}
|
||||||
|
|
||||||
if obj.GetObjectKind().GroupVersionKind().Group != metav1beta1.GroupName {
|
if obj.GetObjectKind().GroupVersionKind().Group != metav1beta1.GroupName {
|
||||||
return nil, fmt.Errorf("attempt to decode non-Table object into a v1beta1.Table")
|
return nil, fmt.Errorf("attempt to decode non-Table object into a v1beta1.Table")
|
||||||
}
|
}
|
||||||
@ -73,5 +79,9 @@ func decodeIntoTable(obj runtime.Object) (runtime.Object, error) {
|
|||||||
row.Object.Object = converted
|
row.Object.Object = converted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isEvent {
|
||||||
|
event.Object.Object = table
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
return table, nil
|
return table, nil
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/duration:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/duration:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||||
"//vendor/github.com/liggitt/tabwriter:go_default_library",
|
"//vendor/github.com/liggitt/tabwriter:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ ResourcePrinter = &HumanReadablePrinter{}
|
var _ ResourcePrinter = &HumanReadablePrinter{}
|
||||||
@ -56,6 +57,7 @@ var (
|
|||||||
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
|
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withEventTypePrefixColumns = []string{"EVENT"}
|
||||||
withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
|
withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,6 +88,12 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||||||
defer w.Flush()
|
defer w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var eventType string
|
||||||
|
if event, isEvent := obj.(*metav1.WatchEvent); isEvent {
|
||||||
|
eventType = event.Type
|
||||||
|
obj = event.Object.Object
|
||||||
|
}
|
||||||
|
|
||||||
// Case 1: Parameter "obj" is a table from server; print it.
|
// Case 1: Parameter "obj" is a table from server; print it.
|
||||||
// display tables following the rules of options
|
// display tables following the rules of options
|
||||||
if table, ok := obj.(*metav1beta1.Table); ok {
|
if table, ok := obj.(*metav1beta1.Table); ok {
|
||||||
@ -112,6 +120,14 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||||||
if err := decorateTable(table, localOptions); err != nil {
|
if err := decorateTable(table, localOptions); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(eventType) > 0 {
|
||||||
|
if err := addColumns(beginning, table,
|
||||||
|
[]metav1beta1.TableColumnDefinition{{Name: "Event", Type: "string"}},
|
||||||
|
[]cellValueFunc{func(metav1beta1.TableRow) (interface{}, error) { return formatEventType(eventType), nil }},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return printTable(table, output, localOptions)
|
return printTable(table, output, localOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +142,7 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||||||
fmt.Fprintln(output)
|
fmt.Fprintln(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printRowsForHandlerEntry(output, handler, obj, h.options, includeHeaders); err != nil {
|
if err := printRowsForHandlerEntry(output, handler, eventType, obj, h.options, includeHeaders); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h.lastType = t
|
h.lastType = t
|
||||||
@ -148,7 +164,7 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
|
|||||||
fmt.Fprintln(output)
|
fmt.Fprintln(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printRowsForHandlerEntry(output, handler, obj, h.options, includeHeaders); err != nil {
|
if err := printRowsForHandlerEntry(output, handler, eventType, obj, h.options, includeHeaders); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h.lastType = handler
|
h.lastType = handler
|
||||||
@ -206,6 +222,75 @@ func printTable(table *metav1beta1.Table, output io.Writer, options PrintOptions
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cellValueFunc func(metav1beta1.TableRow) (interface{}, error)
|
||||||
|
|
||||||
|
type columnAddPosition int
|
||||||
|
|
||||||
|
const (
|
||||||
|
beginning columnAddPosition = 1
|
||||||
|
end columnAddPosition = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func addColumns(pos columnAddPosition, table *metav1beta1.Table, columns []metav1beta1.TableColumnDefinition, valueFuncs []cellValueFunc) error {
|
||||||
|
if len(columns) != len(valueFuncs) {
|
||||||
|
return fmt.Errorf("cannot prepend columns, unmatched value functions")
|
||||||
|
}
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the new rows
|
||||||
|
newRows := make([][]interface{}, len(table.Rows))
|
||||||
|
for i := range table.Rows {
|
||||||
|
newCells := make([]interface{}, 0, len(columns)+len(table.Rows[i].Cells))
|
||||||
|
|
||||||
|
if pos == end {
|
||||||
|
// If we're appending, start with the existing cells,
|
||||||
|
// then add nil cells to match the number of columns
|
||||||
|
newCells = append(newCells, table.Rows[i].Cells...)
|
||||||
|
for len(newCells) < len(table.ColumnDefinitions) {
|
||||||
|
newCells = append(newCells, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute cells for new columns
|
||||||
|
for _, f := range valueFuncs {
|
||||||
|
newCell, err := f(table.Rows[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newCells = append(newCells, newCell)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos == beginning {
|
||||||
|
// If we're prepending, add existing cells
|
||||||
|
newCells = append(newCells, table.Rows[i].Cells...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember the new cells for this row
|
||||||
|
newRows[i] = newCells
|
||||||
|
}
|
||||||
|
|
||||||
|
// All cells successfully computed, now replace columns and rows
|
||||||
|
newColumns := make([]metav1beta1.TableColumnDefinition, 0, len(columns)+len(table.ColumnDefinitions))
|
||||||
|
switch pos {
|
||||||
|
case beginning:
|
||||||
|
newColumns = append(newColumns, columns...)
|
||||||
|
newColumns = append(newColumns, table.ColumnDefinitions...)
|
||||||
|
case end:
|
||||||
|
newColumns = append(newColumns, table.ColumnDefinitions...)
|
||||||
|
newColumns = append(newColumns, columns...)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid column add position: %v", pos)
|
||||||
|
}
|
||||||
|
table.ColumnDefinitions = newColumns
|
||||||
|
for i := range table.Rows {
|
||||||
|
table.Rows[i].Cells = newRows[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// decorateTable takes a table and attempts to add label columns and the
|
// decorateTable takes a table and attempts to add label columns and the
|
||||||
// namespace column. It will fill empty columns with nil (if the object
|
// namespace column. It will fill empty columns with nil (if the object
|
||||||
// does not expose metadata). It returns an error if the table cannot
|
// does not expose metadata). It returns an error if the table cannot
|
||||||
@ -304,7 +389,7 @@ func decorateTable(table *metav1beta1.Table, options PrintOptions) error {
|
|||||||
// printRowsForHandlerEntry prints the incremental table output (headers if the current type is
|
// printRowsForHandlerEntry prints the incremental table output (headers if the current type is
|
||||||
// different from lastType) including all the rows in the object. It returns the current type
|
// different from lastType) including all the rows in the object. It returns the current type
|
||||||
// or an error, if any.
|
// or an error, if any.
|
||||||
func printRowsForHandlerEntry(output io.Writer, handler *handlerEntry, obj runtime.Object, options PrintOptions, includeHeaders bool) error {
|
func printRowsForHandlerEntry(output io.Writer, handler *handlerEntry, eventType string, obj runtime.Object, options PrintOptions, includeHeaders bool) error {
|
||||||
var results []reflect.Value
|
var results []reflect.Value
|
||||||
|
|
||||||
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
|
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
|
||||||
@ -324,23 +409,46 @@ func printRowsForHandlerEntry(output io.Writer, handler *handlerEntry, obj runti
|
|||||||
headers = append(headers, formatLabelHeaders(options.ColumnLabels)...)
|
headers = append(headers, formatLabelHeaders(options.ColumnLabels)...)
|
||||||
// LABELS is always the last column.
|
// LABELS is always the last column.
|
||||||
headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...)
|
headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...)
|
||||||
|
// prepend namespace header
|
||||||
if options.WithNamespace {
|
if options.WithNamespace {
|
||||||
headers = append(withNamespacePrefixColumns, headers...)
|
headers = append(withNamespacePrefixColumns, headers...)
|
||||||
}
|
}
|
||||||
|
// prepend event type header
|
||||||
|
if len(eventType) > 0 {
|
||||||
|
headers = append(withEventTypePrefixColumns, headers...)
|
||||||
|
}
|
||||||
printHeader(headers, output)
|
printHeader(headers, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
if results[1].IsNil() {
|
if results[1].IsNil() {
|
||||||
rows := results[0].Interface().([]metav1beta1.TableRow)
|
rows := results[0].Interface().([]metav1beta1.TableRow)
|
||||||
printRows(output, rows, options)
|
printRows(output, eventType, rows, options)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return results[1].Interface().(error)
|
return results[1].Interface().(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var formattedEventType = map[string]string{
|
||||||
|
string(watch.Added): "ADDED ",
|
||||||
|
string(watch.Modified): "MODIFIED",
|
||||||
|
string(watch.Deleted): "DELETED ",
|
||||||
|
string(watch.Error): "ERROR ",
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatEventType(eventType string) string {
|
||||||
|
if formatted, ok := formattedEventType[eventType]; ok {
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
return string(eventType)
|
||||||
|
}
|
||||||
|
|
||||||
// printRows writes the provided rows to output.
|
// printRows writes the provided rows to output.
|
||||||
func printRows(output io.Writer, rows []metav1beta1.TableRow, options PrintOptions) {
|
func printRows(output io.Writer, eventType string, rows []metav1beta1.TableRow, options PrintOptions) {
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
if len(eventType) > 0 {
|
||||||
|
fmt.Fprint(output, formatEventType(eventType))
|
||||||
|
fmt.Fprint(output, "\t")
|
||||||
|
}
|
||||||
if options.WithNamespace {
|
if options.WithNamespace {
|
||||||
if obj := row.Object.Object; obj != nil {
|
if obj := row.Object.Object; obj != nil {
|
||||||
if m, err := meta.Accessor(obj); err == nil {
|
if m, err := meta.Accessor(obj); err == nil {
|
||||||
|
@ -52,6 +52,7 @@ func TestPrintRowsForHandlerEntry(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
h *handlerEntry
|
h *handlerEntry
|
||||||
opt PrintOptions
|
opt PrintOptions
|
||||||
|
eventType string
|
||||||
obj runtime.Object
|
obj runtime.Object
|
||||||
includeHeader bool
|
includeHeader bool
|
||||||
expectOut string
|
expectOut string
|
||||||
@ -96,6 +97,20 @@ func TestPrintRowsForHandlerEntry(t *testing.T) {
|
|||||||
includeHeader: true,
|
includeHeader: true,
|
||||||
expectOut: "NAME\tSTATUS\tAGE\ntest\t\t<unknow>\n",
|
expectOut: "NAME\tSTATUS\tAGE\ntest\t\t<unknow>\n",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "with event type",
|
||||||
|
h: &handlerEntry{
|
||||||
|
columnDefinitions: testNamespaceColumnDefinitions,
|
||||||
|
printFunc: printFunc,
|
||||||
|
},
|
||||||
|
opt: PrintOptions{},
|
||||||
|
eventType: "ADDED",
|
||||||
|
obj: &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||||
|
},
|
||||||
|
includeHeader: true,
|
||||||
|
expectOut: "EVENT\tNAME\tSTATUS\tAGE\nADDED \ttest\t\t<unknow>\n",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "print namespace and withnamespace true, should not print header",
|
name: "print namespace and withnamespace true, should not print header",
|
||||||
h: &handlerEntry{
|
h: &handlerEntry{
|
||||||
@ -116,7 +131,7 @@ func TestPrintRowsForHandlerEntry(t *testing.T) {
|
|||||||
for _, test := range testCase {
|
for _, test := range testCase {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
buffer := &bytes.Buffer{}
|
buffer := &bytes.Buffer{}
|
||||||
err := printRowsForHandlerEntry(buffer, test.h, test.obj, test.opt, test.includeHeader)
|
err := printRowsForHandlerEntry(buffer, test.h, test.eventType, test.obj, test.opt, test.includeHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() != test.expectErr {
|
if err.Error() != test.expectErr {
|
||||||
t.Errorf("[%s]expect:\n %v\n but got:\n %v\n", test.name, test.expectErr, err)
|
t.Errorf("[%s]expect:\n %v\n but got:\n %v\n", test.name, test.expectErr, err)
|
||||||
|
@ -18,6 +18,7 @@ go_library(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
|
@ -22,7 +22,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
@ -41,6 +43,20 @@ func (p *JSONPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch obj := obj.(type) {
|
switch obj := obj.(type) {
|
||||||
|
case *metav1.WatchEvent:
|
||||||
|
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) {
|
||||||
|
return fmt.Errorf(InternalObjectPrinterErr)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte{'\n'})
|
||||||
|
return err
|
||||||
case *runtime.Unknown:
|
case *runtime.Unknown:
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := json.Indent(&buf, obj.Raw, "", " ")
|
err := json.Indent(&buf, obj.Raw, "", " ")
|
||||||
@ -90,6 +106,20 @@ func (p *YAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch obj := obj.(type) {
|
switch obj := obj.(type) {
|
||||||
|
case *metav1.WatchEvent:
|
||||||
|
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) {
|
||||||
|
return fmt.Errorf(InternalObjectPrinterErr)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err = yaml.JSONToYAML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(data)
|
||||||
|
return err
|
||||||
case *runtime.Unknown:
|
case *runtime.Unknown:
|
||||||
data, err := yaml.JSONToYAML(obj.Raw)
|
data, err := yaml.JSONToYAML(obj.Raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
@ -42,6 +43,11 @@ type NamePrinter struct {
|
|||||||
// PrintObj is an implementation of ResourcePrinter.PrintObj which decodes the object
|
// PrintObj is an implementation of ResourcePrinter.PrintObj which decodes the object
|
||||||
// and print "resource/name" pair. If the object is a List, print all items in it.
|
// and print "resource/name" pair. If the object is a List, print all items in it.
|
||||||
func (p *NamePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
func (p *NamePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||||
|
switch castObj := obj.(type) {
|
||||||
|
case *metav1.WatchEvent:
|
||||||
|
obj = castObj.Object.Object
|
||||||
|
}
|
||||||
|
|
||||||
// we use reflect.Indirect here in order to obtain the actual value from a pointer.
|
// we use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||||
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||||
// we need an actual value in order to retrieve the package path for an object.
|
// we need an actual value in order to retrieve the package path for an object.
|
||||||
|
Loading…
Reference in New Issue
Block a user